diff --git a/VERSION b/VERSION index 3c51832d..697f087f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.27.8 +0.28.0 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 55a8be11..0ca52f6d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -8,6 +8,23 @@ This project follows the repository versioning rule: - `improvement` -> `+0.0.1`(bugfix + 小功能混合) - `bugfix` -> `+0.0.1` +## [0.28.0] — 2026-04-20 + +### ✨ Features +- 将 Earth 的“新闻直播”和“全球态势聚合”合并为统一的“媒体情报”面板,支持底部 tab 切换与共享标题栏操作区 +- 聚合新闻不再单独占据一个 HUD 面板,而是作为媒体情报面板内的第二视图与直播协同呈现 + +### 🔧 Improvements +- TV / News 面板切换加入底边锚定的 reform 动画,并继续保留拖拽、缩放和共享 HUD 行为收口 +- 聚合新闻视图新增默认高度约束与内部滚动填充逻辑,避免初始高度过度膨胀 +- `tv.js`、`news.js` 进一步清理共享 HUDPanel 迁移后的残留逻辑,收紧 tab / resize / reform 相关局部 helper + +### 🐛 Fixes +- 修复媒体情报面板右下角缩放时高度异常抬升、标题栏被顶出视口的问题 +- 修复直播/聚合 tab 切换时按钮高亮、内容切换和底边基准表现不一致的问题 + +--- + ## [0.27.7] — 2026-04-16 ## [0.27.8] — 2026-04-20 diff --git a/docs/version-history.md b/docs/version-history.md index 552c7bd1..06538350 100644 --- a/docs/version-history.md +++ b/docs/version-history.md @@ -16,12 +16,13 @@ ## Current Version - `main` 当前主线历史推导到:`0.16.5` -- `dev` 当前开发分支历史推导到:`0.27.8` +- `dev` 当前开发分支历史推导到:`0.28.0` ## Timeline | Version | Type | Branch | Commit | Summary | | --- | --- | --- | --- | --- | +| `0.28.0` | feature | `dev` | `pending` | 合并 Earth 媒体情报面板,整合新闻直播与态势聚合 tab,并稳定 TV/news 的 reform、resize 与共享 HUD 行为 | | `0.27.8` | bugfix | `dev` | `pending` | 统一 Earth HUD 默认折叠逻辑,修复图例与图层面板箭头和底边阈值行为 | | `0.27.7` | bugfix | `dev` | `pending` | 修复电视直播源编辑持久化问题,清理表格空白占位列并统一可折叠操作列 | | `0.27.6` | improvement | `dev` | `pending` | BGP/用户表格滚动条修复,Playground 响应式按钮与输入框收起优化 | diff --git a/frontend/package.json b/frontend/package.json index 589ca690..c25d646d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "planet-frontend", - "version": "0.27.8", + "version": "0.28.0", "private": true, "packageManager": "bun@1", "dependencies": { diff --git a/frontend/public/earth/css/news-panel.css b/frontend/public/earth/css/news-panel.css index ee63a438..b49a1242 100644 --- a/frontend/public/earth/css/news-panel.css +++ b/frontend/public/earth/css/news-panel.css @@ -1,39 +1,18 @@ /* news-panel.css */ -.hud-panel-news { - --hud-body-max-height: calc(580px * var(--hud-scale)); - top: calc(214px * var(--hud-scale)); - right: var(--hud-offset); - width: calc(420px * var(--hud-scale)); - min-width: calc(320px * var(--hud-scale)); - max-width: calc(100vw - 32px); - padding: calc(12px * var(--hud-scale)); - display: flex; - flex-direction: column; - gap: var(--hud-gap-sm); - z-index: 17; -} - -.news-panel-header { - align-items: center; - gap: var(--hud-gap-xs); - margin-bottom: 0; -} - -.news-panel-header-copy { - flex: 1 1 auto; - min-width: 0; -} - .news-panel-title-row { display: flex; align-items: center; gap: calc(8px * var(--hud-scale)); min-width: 0; + flex: 1 1 auto; } .news-panel-title { margin: 0; + color: var(--hud-text); + font-size: calc(0.92rem * var(--hud-scale)); + font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -58,9 +37,11 @@ .news-panel-body { display: flex; + flex: 1 1 auto; flex-direction: column; gap: var(--hud-gap-sm); - --hud-body-collapse-gap: var(--hud-gap-sm); + min-height: 0; + overflow: hidden; } .news-panel-focus { @@ -104,15 +85,20 @@ .news-board { display: flex; + flex: 1 1 auto; flex-direction: column; gap: var(--hud-gap-sm); + min-height: 0; + overflow: hidden; } .news-board-list { display: flex; + flex: 1 1 auto; flex-direction: column; gap: calc(8px * var(--hud-scale)); - max-height: calc(420px * var(--hud-scale)); + min-height: 0; + max-height: none; overflow-y: auto; padding-right: calc(4px * var(--hud-scale)); scrollbar-width: thin; @@ -205,7 +191,3 @@ line-height: 1.5; padding: calc(16px * var(--hud-scale)) calc(4px * var(--hud-scale)); } - -.earth-app.layout-expanded .hud-panel-news:not([data-dragged="true"]) { - transform: translate(calc(100% - var(--hud-offset)), calc(-100% + var(--hud-offset))); -} diff --git a/frontend/public/earth/css/tv-panel.css b/frontend/public/earth/css/tv-panel.css index 8f0e5e32..f4fa3705 100644 --- a/frontend/public/earth/css/tv-panel.css +++ b/frontend/public/earth/css/tv-panel.css @@ -5,6 +5,7 @@ right: var(--hud-offset); width: calc(420px * var(--hud-scale)); max-width: calc(100vw - 32px); + max-height: calc(100vh - (2 * var(--hud-offset))); min-width: calc(300px * var(--hud-scale)); padding: calc(10px * var(--hud-scale)); display: flex; @@ -13,7 +14,20 @@ z-index: 18; } -/* header 内嵌 select + actions */ +.hud-panel-tv[data-active-tab="news"]:not([data-resized="true"]) { + max-height: min( + var(--tv-news-default-max-height, calc(100vh - (2 * var(--hud-offset)))), + calc(100vh - (2 * var(--hud-offset))) + ); +} + +.hud-panel-tv.is-reforming { + transition: + height 0.24s cubic-bezier(0.22, 1, 0.36, 1), + top 0.24s cubic-bezier(0.22, 1, 0.36, 1); + will-change: height, top; +} + .hud-panel-tv .hud-panel__header { align-items: center; gap: var(--hud-gap-xs); @@ -25,14 +39,53 @@ margin: 0; } -/* select / action 在 drag-handle 内,恢复正常指针 */ -.hud-panel-tv .hud-panel__header .tv-panel-select, .hud-panel-tv .hud-panel__header .hud-panel__action, .hud-panel-tv .hud-panel__header .hud-panel-close { cursor: pointer; user-select: auto; } +.hud-panel-tv .hud-panel__header .tv-panel-select, +.hud-panel-tv .hud-panel__header .tv-panel-tab { + cursor: pointer; + user-select: auto; +} + +.tv-panel-header-controls { + display: flex; + align-items: center; + gap: var(--hud-gap-xs); + flex: 1 1 auto; + min-width: 0; +} + +.tv-panel-header-controls--news { + justify-content: flex-end; +} + +.tv-panel-content { + display: flex; + flex: 1 1 auto; + min-height: 0; + overflow: hidden; +} + +.tv-tab-pane { + display: flex; + flex: 1 1 auto; + min-height: 0; + flex-direction: column; + gap: var(--hud-gap-sm); + overflow: hidden; +} + +.tv-panel-toolbar-actions { + display: inline-flex; + align-items: center; + gap: var(--hud-gap-xs); + flex-shrink: 0; +} + .tv-panel-status { color: var(--hud-text-soft); font-size: calc(0.68rem * var(--hud-scale)); @@ -141,6 +194,41 @@ background: #050a14; } +.tv-panel-tabs { + display: grid; + grid-template-columns: 1fr 1fr; + gap: calc(8px * var(--hud-scale)); +} + +.tv-panel-tab { + border: 1px solid rgba(201, 225, 247, 0.12); + border-radius: calc(12px * var(--hud-scale)); + background: rgba(255, 255, 255, 0.03); + color: var(--hud-text-muted); + padding: calc(9px * var(--hud-scale)) calc(12px * var(--hud-scale)); + font-size: calc(0.78rem * var(--hud-scale)); + font-weight: 600; + letter-spacing: 0.04em; + cursor: pointer; + transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease; +} + +.tv-panel-tab:hover { + color: var(--hud-text); + border-color: rgba(214, 235, 255, 0.18); + background: rgba(255, 255, 255, 0.06); +} + +.tv-panel-tab--active { + color: var(--hud-accent-strong); + border-color: rgba(120, 180, 255, 0.24); + background: rgba(120, 180, 255, 0.12); +} + +.tv-tab-pane[hidden] { + display: none !important; +} + /* ── Multi-edge resize handles ───────────────────────────────── */ .tv-panel-edge { diff --git a/frontend/public/earth/index.html b/frontend/public/earth/index.html index 0ef4aa6f..22145fe3 100644 --- a/frontend/public/earth/index.html +++ b/frontend/public/earth/index.html @@ -297,44 +297,95 @@
- 新闻直播 + 媒体情报 +
+
+ +
+ + + +
+
+ -
- - - -
-
-
- 等待加载直播源 -
暂无可用频道
-
当前未配置可播放新闻直播源
-
频道目录待同步
-
支持后台配置默认源与采集器补充源。
-
+ +
+
+
+
+ 等待加载直播源 +
暂无可用频道
+
当前未配置可播放新闻直播源
+
频道目录待同步
+
支持后台配置默认源与采集器补充源。
+
+
+ +
+
暂无可播放直播源,请先在系统配置中添加频道。
+ + +
+
+ +
-
-
暂无可播放直播源,请先在系统配置中添加频道。
- - + +
+ +
@@ -343,51 +394,6 @@
-
-
-
-
- 全球态势聚合 - global -
-
-
- - - - -
-
- -
-
跟随地球正面视角自动切换区域新闻
- -
-
-
当前关注区域
-
全球焦点
-
跟随当前视角自动聚焦
-
-
0 路聚合源
-
- -
-
正在准备全球态势新闻...
-
- -
-
- -
-
正在初始化全球态势数据...
@@ -451,16 +457,6 @@ -
diff --git a/frontend/public/earth/js/controls.js b/frontend/public/earth/js/controls.js index 7f771dc7..7d0db698 100644 --- a/frontend/public/earth/js/controls.js +++ b/frontend/public/earth/js/controls.js @@ -21,7 +21,6 @@ import { ensureTVPanelReady } from "./tv.js"; import { createHUDPanel } from "./hud-panels.js"; import { ensureNewsPanelReady, - setNewsPanelVisible, updateNewsToggleUI, } from "./news.js"; @@ -37,7 +36,6 @@ const HUD_PANEL_IDS = [ "legend", "earth-stats", "tv-panel", - "news-panel", "layer-toggles", ]; const DRAGGABLE_PANEL_SELECTOR = ".hud-panel-draggable"; @@ -98,20 +96,13 @@ function setHudPanelVisibility(panelId, visible) { syncSettingsToggle(panelId, visible); if (panelId === "tv-panel") { updateTVToggleUI(visible); + updateNewsToggleUI(visible); if (visible) { ensureTVPanelReady().catch((error) => { console.error("初始化电视直播面板失败:", error); }); } } - if (panelId === "news-panel") { - updateNewsToggleUI(visible); - if (visible) { - ensureNewsPanelReady().catch((error) => { - console.error("初始化态势新闻面板失败:", error); - }); - } - } } function syncSettingsToggle(panelId, visible) { @@ -245,7 +236,7 @@ function setupDraggableHudPanels() { }; bindListener(handle, "pointerdown", (event) => { - if (event.target.closest(".hud-panel-close, .hud-panel__action, .layer-panel-btn, .info-card-close, .tv-panel-select, .tv-panel-player, .tv-panel-edge, .legend-bar-btn, .news-story-card")) return; + if (event.target.closest(".hud-panel-close, .hud-panel__action, .layer-panel-btn, .info-card-close, .tv-panel-select, .tv-panel-tab, .tv-panel-player, .tv-panel-edge, .legend-bar-btn, .news-story-card")) return; isDragging = true; startPointerX = event.clientX; startPointerY = event.clientY; @@ -823,14 +814,10 @@ function setupTerrainControls() { console.error("初始化电视直播面板失败:", error); }); } - const newsVisible = !document.getElementById("news-panel")?.classList.contains("hud-panel-hidden"); - updateNewsToggleUI(newsVisible); - setNewsPanelVisible(newsVisible); - if (newsVisible) { - ensureNewsPanelReady().catch((error) => { - console.error("初始化态势新闻面板失败:", error); - }); - } + updateNewsToggleUI(!document.getElementById("tv-panel")?.classList.contains("hud-panel-hidden")); + ensureNewsPanelReady().catch((error) => { + console.error("初始化态势新闻内容失败:", error); + }); updateLayoutUI(container); } diff --git a/frontend/public/earth/js/news.js b/frontend/public/earth/js/news.js index 357250fb..042fab41 100644 --- a/frontend/public/earth/js/news.js +++ b/frontend/public/earth/js/news.js @@ -1,5 +1,5 @@ import { showStatusMessage } from "./ui.js"; -import { createHUDPanel } from "./hud-panels.js"; +import { getActiveTVTab, openTVPanelTab, isTVPanelVisible, setTVPanelVisible } from "./tv.js"; const EARTH_NEWS_API = "/api/v1/news/earth-feed"; const FOCUS_UPDATE_INTERVAL_MS = 4000; @@ -13,15 +13,11 @@ let payload = null; let lastFocus = null; let lastFetchAt = 0; let lastRegionSwitchAt = 0; -let newsPanel = null; - function getElements() { return { - panel: document.getElementById("news-panel"), toggleBtn: document.getElementById("toggle-news"), refreshBtn: document.getElementById("news-refresh"), openBtn: document.getElementById("news-open-external"), - collapseBtn: document.getElementById("news-collapse"), status: document.getElementById("news-board-status"), focusLabel: document.getElementById("news-focus-label"), focusCoords: document.getElementById("news-focus-coords"), @@ -55,28 +51,14 @@ function formatRelativeTime(raw) { export function updateNewsToggleUI(visible) { const { toggleBtn } = getElements(); if (!toggleBtn) return; - toggleBtn.classList.toggle("active", visible); + const active = visible && getActiveTVTab() === "news"; + toggleBtn.classList.toggle("active", active); const tooltip = toggleBtn.querySelector(".earth-toolbar-tooltip"); if (tooltip) { - tooltip.textContent = visible ? "关闭态势新闻" : "打开态势新闻"; + tooltip.textContent = active ? "关闭态势新闻" : "打开态势新闻"; } } -function syncSettingsToggle(visible) { - const input = document.querySelector('[data-settings-panel="news-panel"]'); - if (input instanceof HTMLInputElement) { - input.checked = visible; - } -} - -export function setNewsPanelVisible(visible) { - const { panel } = getElements(); - if (!panel) return; - newsPanel?.setVisible(visible); - updateNewsToggleUI(visible); - syncSettingsToggle(visible); -} - function renderEmptyState(message) { const { board, empty, status, openBtn } = getElements(); if (board) board.innerHTML = ""; @@ -264,10 +246,6 @@ function openCurrentSourceHomepage() { } } -function setCollapsed(collapsed) { - newsPanel?.setCollapsed(collapsed); -} - export function updateNewsViewFocus(coords) { if (!initialized) return; if (!coords || typeof coords.lat !== "number" || typeof coords.lon !== "number") return; @@ -301,34 +279,44 @@ export function initNewsPanel() { if (initialized) return; initialized = true; - const { panel, toggleBtn, refreshBtn, openBtn, collapseBtn } = getElements(); + const { toggleBtn, refreshBtn, openBtn } = getElements(); - if (panel && collapseBtn) { - newsPanel = createHUDPanel({ - panel, - header: ".hud-panel__header", - body: "#news-panel-body", - collapseBtn, - preferredDirection: "down", - expandLabel: "展开新闻面板", - collapseLabel: "折叠新闻面板", - }); - } - - updateNewsToggleUI(true); - syncSettingsToggle(true); + updateNewsToggleUI(isTVPanelVisible()); renderEmptyState("正在准备全球态势新闻聚合源..."); - toggleBtn?.addEventListener("click", async () => { - const nextVisible = !(newsPanel?.isVisible() ?? false); - setNewsPanelVisible(nextVisible); - if (nextVisible) { - try { - await ensureNewsPanelReady(); - } catch { - // surface already handled - } + const openNewsTab = async () => { + openTVPanelTab("news"); + updateNewsToggleUI(true); + try { + await ensureNewsPanelReady(); + } catch { + // surface already handled } + }; + + toggleBtn?.addEventListener("click", async () => { + const visible = isTVPanelVisible(); + const active = visible && getActiveTVTab() === "news"; + + if (!visible) { + await openNewsTab(); + return; + } + + if (!active) { + await openNewsTab(); + return; + } + + setTVPanelVisible(false); + updateNewsToggleUI(false); + }); + + window.addEventListener("earth:tv-tab-change", () => { + updateNewsToggleUI(isTVPanelVisible()); + }); + window.addEventListener("earth:tv-visibility-change", (event) => { + updateNewsToggleUI(Boolean(event.detail?.visible)); }); refreshBtn?.addEventListener("click", async () => { @@ -341,10 +329,6 @@ export function initNewsPanel() { }); openBtn?.addEventListener("click", openCurrentSourceHomepage); - collapseBtn?.addEventListener("click", (event) => { - event.stopPropagation(); - setCollapsed(!newsPanel?.isCollapsed()); - }); refreshNews(undefined, undefined, { silent: true }).catch(() => {}); } diff --git a/frontend/public/earth/js/tv.js b/frontend/public/earth/js/tv.js index 12ac6653..f6ac144b 100644 --- a/frontend/public/earth/js/tv.js +++ b/frontend/public/earth/js/tv.js @@ -23,11 +23,20 @@ let hlsPlayer = null; let hlsRecoveryAttempts = 0; let metaAutoCollapseTimer = null; let tvPanel = null; +let activeTab = "live"; const failedSourceIds = new Set(); let probeTimer = null; +let reformCleanupTimer = null; const META_AUTO_COLLAPSE_DELAY = 2500; const PROBE_INTERVAL_MS = 2 * 60 * 1000; +const DEFAULT_HUD_OFFSET_PX = 20; +const MIN_NEWS_TAB_HEIGHT_PX = 280; +const PANEL_RESIZE_MARGIN_PX = 12; +const TV_PANEL_MIN_WIDTH_PX = 360; +const TV_PANEL_MIN_HEIGHT_PX = 340; +const REFORM_CLEANUP_MS = 280; +const REFORM_RESTORE_ANCHOR_DATA_KEY = "reformRestoreAnchor"; const HLS_MAX_RECOVERY_ATTEMPTS = 3; const HLS_RETRY_CONFIG = { @@ -41,7 +50,6 @@ function getElements() { return { panel: document.getElementById("tv-panel"), toggleBtn: document.getElementById("toggle-tv"), - resizeHandle: null, select: document.getElementById("tv-source-select"), title: document.getElementById("tv-source-title"), meta: document.getElementById("tv-source-meta"), @@ -53,9 +61,14 @@ function getElements() { empty: document.getElementById("tv-empty-state"), refreshBtn: document.getElementById("tv-refresh"), openBtn: document.getElementById("tv-open-external"), - metaPanel: document.getElementById("tv-panel-meta"), metaWrap: document.getElementById("tv-meta-wrap"), metaToggle: document.getElementById("tv-meta-toggle"), + liveHeaderControls: document.getElementById("tv-header-controls-live"), + newsHeaderControls: document.getElementById("tv-header-controls-news"), + liveTabBtn: document.getElementById("tv-tab-live"), + newsTabBtn: document.getElementById("tv-tab-news"), + livePane: document.getElementById("tv-tab-live-pane"), + newsPane: document.getElementById("tv-tab-news-pane"), }; } @@ -63,6 +76,37 @@ function setMetaCollapsed(collapsed) { tvPanel?.setCollapsed(collapsed); } +function syncPanelActiveTab(tab = activeTab) { + const { panel } = getElements(); + if (panel instanceof HTMLElement) { + panel.dataset.activeTab = tab; + } +} + +function syncNewsDefaultMaxHeight() { + const { panel } = getElements(); + if (!(panel instanceof HTMLElement)) return; + + const earthStats = document.getElementById("earth-stats"); + const hudOffset = Number.parseFloat( + getComputedStyle(document.documentElement).getPropertyValue("--hud-offset"), + ); + const resolvedOffset = Number.isFinite(hudOffset) ? hudOffset : DEFAULT_HUD_OFFSET_PX; + + if (!(earthStats instanceof HTMLElement)) { + panel.style.removeProperty("--tv-news-default-max-height"); + return; + } + + const statsRect = earthStats.getBoundingClientRect(); + const availableHeight = Math.max( + Math.round(MIN_NEWS_TAB_HEIGHT_PX * getHudScale()), + Math.floor(window.innerHeight - resolvedOffset - statsRect.bottom), + ); + + panel.style.setProperty("--tv-news-default-max-height", `${availableHeight}px`); +} + function autoExpandMeta() { clearTimeout(metaAutoCollapseTimer); setMetaCollapsed(false); @@ -92,12 +136,14 @@ function setupResizeHandle() { let resizing = false; let activeEdge = ""; - let startX = 0; - let startY = 0; - let startWidth = 0; - let startHeight = 0; - let startLeft = 0; - let startTop = 0; + const resizeStart = { + pointerX: 0, + pointerY: 0, + width: 0, + height: 0, + left: 0, + top: 0, + }; const stopResize = () => { resizing = false; @@ -110,23 +156,23 @@ function setupResizeHandle() { if (!resizing) return; const containerRect = container.getBoundingClientRect(); const hudScale = getHudScale(); - const minWidth = Math.max(320, Math.round(360 * hudScale)); - const minHeight = Math.max(260, Math.round(340 * hudScale)); - const dx = event.clientX - startX; - const dy = event.clientY - startY; + const minWidth = Math.max(320, Math.round(TV_PANEL_MIN_WIDTH_PX * hudScale)); + const minHeight = Math.max(260, Math.round(TV_PANEL_MIN_HEIGHT_PX * hudScale)); + const dx = event.clientX - resizeStart.pointerX; + const dy = event.clientY - resizeStart.pointerY; if (activeEdge.includes("r")) { - const maxW = containerRect.width - startLeft - 12; - panel.style.width = `${Math.min(maxW, Math.max(minWidth, startWidth + dx))}px`; + const maxW = containerRect.width - resizeStart.left - PANEL_RESIZE_MARGIN_PX; + panel.style.width = `${Math.min(maxW, Math.max(minWidth, resizeStart.width + dx))}px`; } if (activeEdge.includes("l")) { - const newW = Math.max(minWidth, startWidth - dx); + const newW = Math.max(minWidth, resizeStart.width - dx); panel.style.width = `${newW}px`; - panel.style.left = `${Math.max(0, startLeft + startWidth - newW)}px`; + panel.style.left = `${Math.max(0, resizeStart.left + resizeStart.width - newW)}px`; } if (activeEdge.includes("b")) { - const maxH = containerRect.height - startTop - 12; - panel.style.minHeight = `${Math.min(maxH, Math.max(minHeight, startHeight + dy))}px`; + const maxH = containerRect.height - resizeStart.top - PANEL_RESIZE_MARGIN_PX; + panel.style.height = `${Math.min(maxH, Math.max(minHeight, resizeStart.height + dy))}px`; } }; @@ -138,17 +184,21 @@ function setupResizeHandle() { activeEdge = edgeEl.dataset.edge ?? ""; resizing = true; - startX = event.clientX; - startY = event.clientY; + resizeStart.pointerX = event.clientX; + resizeStart.pointerY = event.clientY; clearPanelPositioningForResize(panel); + panel.dataset.resized = "true"; const rect = panel.getBoundingClientRect(); const cRect = container.getBoundingClientRect(); - startWidth = rect.width; - startHeight = rect.height; - startLeft = rect.left - cRect.left; - startTop = rect.top - cRect.top; + resizeStart.width = rect.width; + resizeStart.height = rect.height; + resizeStart.left = rect.left - cRect.left; + resizeStart.top = rect.top - cRect.top; + panel.style.width = `${resizeStart.width}px`; + panel.style.height = `${resizeStart.height}px`; + panel.style.minHeight = ""; panel.classList.add("is-resizing"); document.body.style.userSelect = "none"; @@ -165,10 +215,11 @@ function setupResizeHandle() { function updateToggleButton(visible) { const { toggleBtn } = getElements(); if (!toggleBtn) return; - toggleBtn.classList.toggle("active", visible); + const active = visible && activeTab === "live"; + toggleBtn.classList.toggle("active", active); const tooltip = toggleBtn.querySelector(".earth-toolbar-tooltip"); if (tooltip) { - tooltip.textContent = visible ? "关闭新闻直播" : "打开新闻直播"; + tooltip.textContent = active ? "关闭新闻直播" : "打开新闻直播"; } } @@ -185,6 +236,151 @@ function setPanelVisible(visible) { tvPanel?.setVisible(visible); updateToggleButton(visible); syncSettingsToggle(visible); + window.dispatchEvent(new CustomEvent("earth:tv-visibility-change", { + detail: { visible }, + })); +} + +export function setTVPanelVisible(visible) { + setPanelVisible(visible); +} + +function clearReformState() { + const { panel } = getElements(); + if (!(panel instanceof HTMLElement)) return; + panel.classList.remove("is-reforming"); + panel.style.height = ""; + if (panel.dataset[REFORM_RESTORE_ANCHOR_DATA_KEY] === "true") { + panel.style.top = ""; + panel.style.bottom = ""; + delete panel.dataset[REFORM_RESTORE_ANCHOR_DATA_KEY]; + } + if (reformCleanupTimer) { + clearTimeout(reformCleanupTimer); + reformCleanupTimer = null; + } +} + +function animateTabReform(applyChange) { + const { panel } = getElements(); + const container = document.getElementById("container"); + if (!(panel instanceof HTMLElement)) { + applyChange(); + return; + } + if (!(container instanceof HTMLElement)) { + applyChange(); + return; + } + + if (panel.classList.contains("is-dragging") || panel.classList.contains("is-resizing")) { + applyChange(); + return; + } + + clearReformState(); + + const reformStartRect = panel.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); + const reformSnapshot = { + height: reformStartRect.height, + anchoredBottom: reformStartRect.bottom - containerRect.top, + shouldRestoreDefaultAnchoring: panel.dataset.dragged !== "true", + }; + + if (reformSnapshot.shouldRestoreDefaultAnchoring) { + panel.dataset[REFORM_RESTORE_ANCHOR_DATA_KEY] = "true"; + panel.style.bottom = "auto"; + } + + panel.style.top = `${reformSnapshot.anchoredBottom - reformSnapshot.height}px`; + panel.style.height = `${reformSnapshot.height}px`; + panel.classList.add("is-reforming"); + void panel.offsetHeight; + + applyChange(); + + panel.style.height = "auto"; + const targetHeight = panel.getBoundingClientRect().height; + panel.style.height = `${reformSnapshot.height}px`; + void panel.offsetHeight; + const targetTop = reformSnapshot.anchoredBottom - targetHeight; + + const finalizeReform = () => { + panel.removeEventListener("transitionend", handleReformTransitionEnd); + clearReformState(); + }; + + const handleReformTransitionEnd = (event) => { + if (event.target === panel && event.propertyName === "height") { + finalizeReform(); + } + }; + + panel.addEventListener("transitionend", handleReformTransitionEnd); + reformCleanupTimer = window.setTimeout(finalizeReform, REFORM_CLEANUP_MS); + + requestAnimationFrame(() => { + panel.style.top = `${targetTop}px`; + panel.style.height = `${targetHeight}px`; + }); +} + +function updateTabState(target, isActive, activeClassName = "") { + if (!(target instanceof HTMLElement)) return; + target.hidden = !isActive; + if (activeClassName) { + target.classList.toggle(activeClassName, isActive); + } +} + +function updateTabButtonState(button, isActive) { + if (!(button instanceof HTMLButtonElement)) return; + button.classList.toggle("tv-panel-tab--active", isActive); + button.setAttribute("aria-selected", isActive ? "true" : "false"); +} + +function setActiveTab(tab) { + const nextTab = tab === "news" ? "news" : "live"; + if (activeTab === nextTab) return; + + animateTabReform(() => { + activeTab = nextTab; + syncPanelActiveTab(nextTab); + syncNewsDefaultMaxHeight(); + const { + liveTabBtn, + newsTabBtn, + liveHeaderControls, + newsHeaderControls, + livePane, + newsPane, + } = getElements(); + + updateTabButtonState(liveTabBtn, nextTab === "live"); + updateTabButtonState(newsTabBtn, nextTab === "news"); + updateTabState(liveHeaderControls, nextTab === "live"); + updateTabState(newsHeaderControls, nextTab === "news"); + updateTabState(livePane, nextTab === "live", "tv-tab-pane--active"); + updateTabState(newsPane, nextTab === "news", "tv-tab-pane--active"); + window.dispatchEvent(new CustomEvent("earth:tv-tab-change", { + detail: { tab: nextTab }, + })); + }); +} + +export function openTVPanelTab(tab = "live") { + setPanelVisible(true); + if (activeTab === tab) return; + setActiveTab(tab); +} + +export function isTVPanelVisible() { + return tvPanel?.isVisible() ?? !getElements().panel?.classList.contains("hud-panel-hidden"); +} + +export function getActiveTVTab() { + return activeTab; } function getEmbeddedUrl(source) { @@ -652,7 +848,17 @@ export function initTVPanel() { if (initialized) return; initialized = true; - const { select, refreshBtn, iframe, video, toggleBtn, panel, metaToggle } = getElements(); + const { + select, + refreshBtn, + iframe, + video, + toggleBtn, + panel, + metaToggle, + liveTabBtn, + newsTabBtn, + } = getElements(); if (panel && metaToggle) { tvPanel = createHUDPanel({ @@ -673,14 +879,23 @@ export function initTVPanel() { toggleBtn?.addEventListener("click", async (event) => { event.preventDefault(); event.stopPropagation(); - const nextVisible = !(tvPanel?.isVisible() ?? false); - setPanelVisible(nextVisible); - if (nextVisible) { + const currentlyVisible = tvPanel?.isVisible() ?? false; + if (!currentlyVisible) { + setPanelVisible(true); + setActiveTab("live"); await ensureTVPanelReady(); showStatusMessage("新闻直播窗口已打开", "info"); - } else { - showStatusMessage("新闻直播窗口已关闭", "info"); + return; } + + if (activeTab !== "live") { + setActiveTab("live"); + showStatusMessage("已切换到新闻直播", "info"); + return; + } + + setPanelVisible(false); + showStatusMessage("新闻直播窗口已关闭", "info"); }); select?.addEventListener("change", (event) => { @@ -700,6 +915,13 @@ export function initTVPanel() { refreshTVPanel(); }); + liveTabBtn?.addEventListener("click", () => { + setActiveTab("live"); + }); + newsTabBtn?.addEventListener("click", () => { + setActiveTab("news"); + }); + iframe?.addEventListener("load", () => { if (iframe.hidden) return; clearSourceFailed(currentSourceId); @@ -720,4 +942,9 @@ export function initTVPanel() { }); setupResizeHandle(); + syncPanelActiveTab("live"); + syncNewsDefaultMaxHeight(); + setActiveTab("live"); + + window.addEventListener("resize", syncNewsDefaultMaxHeight); } diff --git a/pyproject.toml b/pyproject.toml index 85293cd6..db6e2c42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "planet" -version = "0.27.8" +version = "0.28.0" description = "智能星球计划 - 态势感知系统" requires-python = ">=3.14" dependencies = [ diff --git a/uv.lock b/uv.lock index d09d7cbb..b6e4dda5 100644 --- a/uv.lock +++ b/uv.lock @@ -475,7 +475,7 @@ wheels = [ [[package]] name = "planet" -version = "0.27.8" +version = "0.28.0" source = { virtual = "." } dependencies = [ { name = "aiofiles" },