release: bump version to 0.28.0

This commit is contained in:
linkong
2026-04-20 14:10:27 +08:00
parent 51ae5e6ec9
commit 4c21973197
12 changed files with 512 additions and 230 deletions

View File

@@ -1 +1 @@
0.27.8
0.28.0

View File

@@ -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

View File

@@ -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 响应式按钮与输入框收起优化 |

View File

@@ -1,6 +1,6 @@
{
"name": "planet-frontend",
"version": "0.27.8",
"version": "0.28.0",
"private": true,
"packageManager": "bun@1",
"dependencies": {

View File

@@ -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)));
}

View File

@@ -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 {

View File

@@ -297,44 +297,95 @@
<div id="tv-panel" class="hud-panel hud-panel-tv hud-panel-draggable" data-panel-key="tv-panel" data-drag-self="true">
<div class="hud-panel__header hud-panel-drag-handle">
<div class="hud-panel__title-group">
<span class="hud-panel-title hud-panel__title tv-panel-header-title">新闻直播</span>
<span class="hud-panel-title hud-panel__title tv-panel-header-title">媒体情报</span>
</div>
<div id="tv-header-controls-live" class="tv-panel-header-controls tv-panel-header-controls--live">
<select id="tv-source-select" class="tv-panel-select" aria-label="选择新闻直播源"></select>
<div class="tv-panel-toolbar-actions">
<button id="tv-refresh" class="hud-panel__action hud-panel__action--refresh" type="button" title="刷新直播源" aria-label="刷新直播源">
<span class="material-symbols-rounded">refresh</span>
</button>
<button id="tv-open-external" class="hud-panel__action hud-panel__action--external" type="button" title="访问官网" aria-label="访问官网">
<span class="material-symbols-rounded">open_in_new</span>
</button>
<button id="tv-meta-toggle" class="hud-panel__action hud-panel__action--collapse tv-panel-meta-toggle" type="button" title="折叠新闻直播内容" aria-label="折叠新闻直播内容">
<span class="material-symbols-rounded">expand_less</span>
</button>
</div>
</div>
<div id="tv-header-controls-news" class="tv-panel-header-controls tv-panel-header-controls--news" hidden>
<div class="news-panel-title-row">
<span class="news-panel-title">全球态势聚合</span>
<span id="news-region-chip" class="news-region-chip hud-panel__chip">global</span>
</div>
<div class="tv-panel-toolbar-actions">
<button id="news-refresh" class="hud-panel__action hud-panel__action--refresh" type="button" title="刷新新闻源" aria-label="刷新新闻源">
<span class="material-symbols-rounded">refresh</span>
</button>
<button id="news-open-external" class="hud-panel__action hud-panel__action--external" type="button" title="打开源站" aria-label="打开源站">
<span class="material-symbols-rounded">open_in_new</span>
</button>
</div>
</div>
<select id="tv-source-select" class="tv-panel-select" aria-label="选择新闻直播源"></select>
<div class="hud-panel__actions">
<button id="tv-refresh" class="hud-panel__action hud-panel__action--refresh" type="button" title="刷新直播源" aria-label="刷新直播源">
<span class="material-symbols-rounded">refresh</span>
</button>
<button id="tv-open-external" class="hud-panel__action hud-panel__action--external" type="button" title="访问官网" aria-label="访问官网">
<span class="material-symbols-rounded">open_in_new</span>
</button>
<button id="tv-meta-toggle" class="hud-panel__action hud-panel__action--collapse tv-panel-meta-toggle" type="button" title="折叠新闻直播内容" aria-label="折叠新闻直播内容">
<span class="material-symbols-rounded">expand_less</span>
</button>
<button class="hud-panel-close hud-panel__action hud-panel__action--close" type="button" data-close-panel="tv-panel" aria-label="关闭电视直播">
<button class="hud-panel-close hud-panel__action hud-panel__action--close" type="button" data-close-panel="tv-panel" aria-label="关闭媒体情报面板">
<span class="material-symbols-rounded">close</span>
</button>
</div>
</div>
<div class="tv-panel-meta-wrap" id="tv-meta-wrap">
<div class="tv-panel-meta" id="tv-panel-meta">
<span id="tv-source-status" class="tv-panel-status">等待加载直播源</span>
<div id="tv-source-title" class="tv-panel-title">暂无可用频道</div>
<div id="tv-source-meta" class="tv-panel-subtitle">当前未配置可播放新闻直播源</div>
<div id="tv-source-catalog" class="tv-panel-catalog">频道目录待同步</div>
<div id="tv-source-notes" class="tv-panel-notes">支持后台配置默认源与采集器补充源。</div>
</div>
<div class="tv-panel-content">
<section id="tv-tab-live-pane" class="tv-tab-pane tv-tab-pane--active" aria-labelledby="tv-tab-live">
<div class="tv-panel-meta-wrap" id="tv-meta-wrap">
<div class="tv-panel-meta" id="tv-panel-meta">
<span id="tv-source-status" class="tv-panel-status">等待加载直播源</span>
<div id="tv-source-title" class="tv-panel-title">暂无可用频道</div>
<div id="tv-source-meta" class="tv-panel-subtitle">当前未配置可播放新闻直播源</div>
<div id="tv-source-catalog" class="tv-panel-catalog">频道目录待同步</div>
<div id="tv-source-notes" class="tv-panel-notes">支持后台配置默认源与采集器补充源。</div>
</div>
</div>
<div class="tv-panel-player">
<div id="tv-empty-state" class="tv-panel-empty">暂无可播放直播源,请先在系统配置中添加频道。</div>
<iframe
id="tv-iframe"
class="tv-panel-iframe"
hidden
title="新闻直播"
referrerpolicy="strict-origin-when-cross-origin"
allow="autoplay; fullscreen; picture-in-picture"
></iframe>
<video id="tv-video" class="tv-panel-video" hidden controls autoplay muted playsinline></video>
</div>
</section>
<section id="tv-tab-news-pane" class="tv-tab-pane tv-tab-pane--news" aria-labelledby="tv-tab-news" hidden>
<div id="news-panel-body" class="news-panel-body">
<div class="news-panel-subtitle">跟随地球正面视角自动切换区域新闻</div>
<div class="news-panel-focus">
<div>
<div class="news-focus-kicker">当前关注区域</div>
<div id="news-focus-label" class="news-focus-label">全球焦点</div>
<div id="news-focus-coords" class="news-focus-coords">跟随当前视角自动聚焦</div>
</div>
<div id="news-source-count" class="news-source-count">0 路聚合源</div>
</div>
<div class="news-board">
<div id="news-board-status" class="news-board-status">正在准备全球态势新闻...</div>
<div id="news-board-list" class="news-board-list"></div>
<div id="news-board-empty" class="news-board-empty" hidden>正在准备全球态势新闻聚合源...</div>
</div>
</div>
<a id="news-feed-anchor" hidden rel="noreferrer noopener" target="_blank"></a>
</section>
</div>
<div class="tv-panel-player">
<div id="tv-empty-state" class="tv-panel-empty">暂无可播放直播源,请先在系统配置中添加频道。</div>
<iframe
id="tv-iframe"
class="tv-panel-iframe"
hidden
title="新闻直播"
referrerpolicy="strict-origin-when-cross-origin"
allow="autoplay; fullscreen; picture-in-picture"
></iframe>
<video id="tv-video" class="tv-panel-video" hidden controls autoplay muted playsinline></video>
<div class="tv-panel-tabs" role="tablist" aria-label="媒体情报切换">
<button id="tv-tab-live" class="tv-panel-tab tv-panel-tab--active" type="button" role="tab" aria-selected="true" aria-controls="tv-tab-live-pane">直播</button>
<button id="tv-tab-news" class="tv-panel-tab" type="button" role="tab" aria-selected="false" aria-controls="tv-tab-news-pane">聚合</button>
</div>
<div class="tv-panel-edge" data-edge="r"></div>
<div class="tv-panel-edge" data-edge="b"></div>
@@ -343,51 +394,6 @@
<div class="tv-panel-edge" data-edge="bl"></div>
</div>
<div id="news-panel" class="hud-panel hud-panel-news hud-panel-draggable hud-panel--expand-down" data-panel-key="news-panel">
<div class="hud-panel__header hud-panel-drag-handle">
<div class="news-panel-header-copy hud-panel__title-group">
<div class="news-panel-title-row">
<span class="hud-panel-title hud-panel__title news-panel-title">全球态势聚合</span>
<span id="news-region-chip" class="news-region-chip hud-panel__chip">global</span>
</div>
</div>
<div class="hud-panel__actions">
<button id="news-refresh" class="hud-panel__action hud-panel__action--refresh" type="button" title="刷新新闻源" aria-label="刷新新闻源">
<span class="material-symbols-rounded">refresh</span>
</button>
<button id="news-open-external" class="hud-panel__action hud-panel__action--external" type="button" title="打开源站" aria-label="打开源站">
<span class="material-symbols-rounded">open_in_new</span>
</button>
<button id="news-collapse" class="hud-panel__action hud-panel__action--collapse news-panel-collapse" type="button" title="折叠新闻面板" aria-label="折叠新闻面板">
<span class="material-symbols-rounded">expand_less</span>
</button>
<button class="hud-panel__action hud-panel__action--close hud-panel-close" type="button" data-close-panel="news-panel" aria-label="关闭态势新闻">
<span class="material-symbols-rounded">close</span>
</button>
</div>
</div>
<div id="news-panel-body" class="news-panel-body hud-panel__body hud-panel__body--collapsible">
<div class="news-panel-subtitle">跟随地球正面视角自动切换区域新闻</div>
<div class="news-panel-focus">
<div>
<div class="news-focus-kicker">当前关注区域</div>
<div id="news-focus-label" class="news-focus-label">全球焦点</div>
<div id="news-focus-coords" class="news-focus-coords">跟随当前视角自动聚焦</div>
</div>
<div id="news-source-count" class="news-source-count">0 路聚合源</div>
</div>
<div class="news-board">
<div id="news-board-status" class="news-board-status">正在准备全球态势新闻...</div>
<div id="news-board-list" class="news-board-list"></div>
<div id="news-board-empty" class="news-board-empty" hidden>正在准备全球态势新闻聚合源...</div>
</div>
</div>
<a id="news-feed-anchor" hidden rel="noreferrer noopener" target="_blank"></a>
</div>
<div id="loading" class="earth-loading">
<div id="loading-spinner" class="earth-loading-spinner"></div>
<div id="loading-title" class="earth-loading-title earth-loading-text">正在初始化全球态势数据...</div>
@@ -451,16 +457,6 @@
<span class="earth-settings-switch-track"></span>
</span>
</label>
<label class="earth-settings-item" for="toggle-view-news">
<div class="earth-settings-copy">
<span class="earth-settings-item-title">态势新闻</span>
<span class="earth-settings-item-subtitle">控制跟随视角切换的全球态势 RSS 看板显示</span>
</div>
<span class="earth-settings-switch">
<input id="toggle-view-news" type="checkbox" data-settings-panel="news-panel" checked>
<span class="earth-settings-switch-track"></span>
</span>
</label>
</div>
</section>
</div>

View File

@@ -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);
}

View File

@@ -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(() => {});
}

View File

@@ -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);
}

View File

@@ -1,6 +1,6 @@
[project]
name = "planet"
version = "0.27.8"
version = "0.28.0"
description = "智能星球计划 - 态势感知系统"
requires-python = ">=3.14"
dependencies = [

2
uv.lock generated
View File

@@ -475,7 +475,7 @@ wheels = [
[[package]]
name = "planet"
version = "0.27.8"
version = "0.28.0"
source = { virtual = "." }
dependencies = [
{ name = "aiofiles" },