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 @@
-
+
+
+
+
-
-
-
-
-
-
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" },