release: bump version to 0.29.1

This commit is contained in:
rayd1o
2026-04-20 22:12:19 +08:00
parent fe45a99cbd
commit e6d0332fba
15 changed files with 360 additions and 180 deletions

View File

@@ -1 +1 @@
0.29.0 0.29.1

View File

@@ -8,6 +8,23 @@ This project follows the repository versioning rule:
- `improvement` -> `+0.0.1`bugfix + 小功能混合) - `improvement` -> `+0.0.1`bugfix + 小功能混合)
- `bugfix` -> `+0.0.1` - `bugfix` -> `+0.0.1`
## [0.29.1] — 2026-04-20
### ✨ Highlights
- Earth 加载状态条改成单一队列式通知面板,加载阶段不再因为步骤文案变化而回缩,也不会被其他通知打断
- 调整 brand panel 的呈现方式与昼夜/选中态可读性,让品牌区更自然、交互高亮在白天和黑夜里都更稳定
### 🔧 Improvements
- 移除旧的地球加载浮层结构,统一由 HUD 状态消息承载三点脉冲加载过程
- brand panel 改为无边框品牌层,仅保留轻微氛围光,不再因为非常规尺寸显得像第五块功能面板
- 温和收敛地球昼夜材质与主背光强度,保留昼夜辨识度的同时提升白天地表纹理和夜面交互可见性
### 🐛 Fixes
- 修复加载地球时通知条在步骤切换中反复缩短、其他状态消息抢占加载流程的问题
- 修复海缆、登陆点和 BGP 选中高亮在黑夜中过暗、在高光中过亮导致难以辨识的问题
---
## [0.29.0] — 2026-04-20 ## [0.29.0] — 2026-04-20
### ✨ Highlights ### ✨ Highlights

View File

@@ -16,12 +16,13 @@
## Current Version ## Current Version
- `main` 当前主线历史推导到:`0.16.5` - `main` 当前主线历史推导到:`0.16.5`
- `dev` 当前开发分支历史推导到:`0.29.0` - `dev` 当前开发分支历史推导到:`0.29.1`
## Timeline ## Timeline
| Version | Type | Branch | Commit | Summary | | Version | Type | Branch | Commit | Summary |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| `0.29.1` | bugfix | `dev` | `pending` | Earth 加载通知条改为队列式单面板显示brand panel 去框并收敛昼夜与选中态可读性 |
| `0.29.0` | feature | `dev` | `pending` | Earth 新增天球背景与太阳/月亮位置层,强化昼夜分隔并收口卫星图例与图层面板交互 | | `0.29.0` | feature | `dev` | `pending` | Earth 新增天球背景与太阳/月亮位置层,强化昼夜分隔并收口卫星图例与图层面板交互 |
| `0.28.2` | bugfix | `dev` | `pending` | 修正媒体情报 tab 尺寸记忆与切换锚点逻辑,并清理 docs 根目录遗留旧路径文档 | | `0.28.2` | bugfix | `dev` | `pending` | 修正媒体情报 tab 尺寸记忆与切换锚点逻辑,并清理 docs 根目录遗留旧路径文档 |
| `0.28.1` | bugfix | `dev` | `pending` | 收口 Earth 媒体情报面板命名与 tab 文案,整理 docs 分组并归档已完成/废弃计划文档 | | `0.28.1` | bugfix | `dev` | `pending` | 收口 Earth 媒体情报面板命名与 tab 文案,整理 docs 分组并归档已完成/废弃计划文档 |

View File

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

View File

@@ -83,59 +83,17 @@ body.earth-page {
pointer-events: none; pointer-events: none;
} }
.earth-loading { @keyframes earthLoadingPulse {
position: absolute; 0%,
top: 50%; 80%,
left: 50%; 100% {
transform: translate(-50%, -50%); opacity: 0.28;
z-index: 240; transform: scale(0.78);
min-width: min(calc(320px * var(--hud-scale)), 78vw);
padding: calc(26px * var(--hud-scale));
border-radius: calc(18px * var(--hud-scale));
border: 1px solid rgba(77, 184, 255, 0.34);
background:
radial-gradient(circle at 50% 18%, rgba(255, 255, 255, 0.12), transparent 35%),
linear-gradient(180deg, rgba(13, 24, 46, 0.95), rgba(7, 14, 28, 0.94));
box-shadow:
0 0 30px rgba(77, 184, 255, 0.22),
0 16px 40px rgba(0, 0, 0, 0.28);
text-align: center;
color: #4db8ff;
}
.earth-loading-text {
color: #4db8ff;
}
.earth-loading-title {
font-size: calc(1.15rem * var(--hud-scale));
font-weight: 600;
}
.earth-loading-subtitle {
margin-top: calc(10px * var(--hud-scale));
color: #9ab7d4;
font-size: calc(0.84rem * var(--hud-scale));
line-height: 1.45;
}
.earth-loading-spinner {
width: calc(40px * var(--hud-scale));
height: calc(40px * var(--hud-scale));
margin: 0 auto calc(15px * var(--hud-scale));
border: 4px solid rgba(77, 184, 255, 0.28);
border-top: 4px solid #4db8ff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
} }
100% { 40% {
transform: rotate(360deg); opacity: 1;
transform: scale(1);
} }
} }

View File

@@ -243,21 +243,32 @@
.earth-status-message { .earth-status-message {
position: absolute; position: absolute;
top: 20px; top: calc(20px * var(--hud-scale));
left: 50%; left: 50%;
transform: translate(-50%, -18px); transform: translate(-50%, -18px);
background:
linear-gradient(180deg, rgba(18, 31, 52, 0.92), rgba(8, 18, 32, 0.9));
border-radius: 14px;
padding: 11px 15px;
z-index: 210;
box-shadow: var(--hud-shadow-soft);
border: 1px solid var(--hud-border);
font-size: 0.9rem;
display: none; display: none;
backdrop-filter: blur(10px); align-items: center;
text-align: center; gap: calc(10px * var(--hud-scale));
min-width: 180px; background:
linear-gradient(90deg, rgba(145, 186, 255, 0.07) 0%, transparent 44%),
linear-gradient(180deg, rgba(22, 36, 58, 0.95), rgba(8, 18, 32, 0.93));
border-radius: 999px;
border: 1px solid var(--hud-border);
border-left-color: rgba(145, 186, 255, 0.32);
padding: calc(8px * var(--hud-scale)) calc(20px * var(--hud-scale)) calc(8px * var(--hud-scale)) calc(16px * var(--hud-scale));
z-index: 210;
box-shadow:
var(--hud-shadow-soft),
0 0 18px rgba(145, 186, 255, 0.06);
font-size: calc(0.84rem * var(--hud-scale));
font-weight: 500;
letter-spacing: 0.01em;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
text-align: left;
min-width: min(calc(160px * var(--hud-scale)), 58vw);
max-width: min(calc(440px * var(--hud-scale)), 74vw);
color: var(--hud-text);
opacity: 0; opacity: 0;
transition: transition:
transform 0.28s ease, transform 0.28s ease,
@@ -269,19 +280,110 @@
opacity: 1; opacity: 1;
} }
/* ── Indicator: single dot (transient) or three dots (loading) ── */
.earth-status-indicator {
display: inline-flex;
align-items: center;
gap: calc(5px * var(--hud-scale));
flex: 0 0 auto;
}
.earth-status-dot {
width: calc(7px * var(--hud-scale));
height: calc(7px * var(--hud-scale));
border-radius: 50%;
background: rgba(145, 186, 255, 0.95);
box-shadow:
0 0 8px rgba(145, 186, 255, 0.7),
0 0 20px rgba(145, 186, 255, 0.28);
animation: statusDotPulse 2.4s ease-in-out infinite;
}
.earth-status-text {
flex: 1 1 auto;
min-width: 0;
}
/* Loading: three-dot sequential pulse */
.earth-status-message.loading .earth-status-dot {
animation: earthLoadingPulse 1.2s ease-in-out infinite;
}
.earth-status-message.loading .earth-status-dot:nth-child(2) {
animation-delay: 0.16s;
}
.earth-status-message.loading .earth-status-dot:nth-child(3) {
animation-delay: 0.32s;
}
/* ── Status color variants ───────────────────────────────────── */
.earth-status-message.success { .earth-status-message.success {
color: #d8f7df; color: #dff8e7;
border-left: 3px solid #66d18f; background:
linear-gradient(90deg, rgba(102, 209, 143, 0.08) 0%, transparent 44%),
linear-gradient(180deg, rgba(22, 36, 58, 0.95), rgba(8, 18, 32, 0.93));
border-left-color: rgba(102, 209, 143, 0.38);
box-shadow:
var(--hud-shadow-soft),
0 0 18px rgba(102, 209, 143, 0.06);
}
.earth-status-message.success .earth-status-dot {
background: #66d18f;
box-shadow:
0 0 8px rgba(102, 209, 143, 0.8),
0 0 20px rgba(102, 209, 143, 0.3);
} }
.earth-status-message.warning { .earth-status-message.warning {
color: #fff2c3; color: #fff2c3;
border-left: 3px solid #e4c464; background:
linear-gradient(90deg, rgba(228, 196, 100, 0.08) 0%, transparent 44%),
linear-gradient(180deg, rgba(22, 36, 58, 0.95), rgba(8, 18, 32, 0.93));
border-left-color: rgba(228, 196, 100, 0.38);
box-shadow:
var(--hud-shadow-soft),
0 0 18px rgba(228, 196, 100, 0.06);
}
.earth-status-message.warning .earth-status-dot {
background: #e4c464;
box-shadow:
0 0 8px rgba(228, 196, 100, 0.8),
0 0 20px rgba(228, 196, 100, 0.3);
} }
.earth-status-message.error { .earth-status-message.error {
color: #ffd4d7; color: #ffd4d7;
border-left: 3px solid #ff7b86; background:
linear-gradient(90deg, rgba(255, 123, 134, 0.08) 0%, transparent 44%),
linear-gradient(180deg, rgba(22, 36, 58, 0.95), rgba(8, 18, 32, 0.93));
border-left-color: rgba(255, 123, 134, 0.38);
box-shadow:
var(--hud-shadow-soft),
0 0 18px rgba(255, 123, 134, 0.06);
}
.earth-status-message.error .earth-status-dot {
background: #ff7b86;
box-shadow:
0 0 8px rgba(255, 123, 134, 0.8),
0 0 20px rgba(255, 123, 134, 0.34);
}
@keyframes statusDotPulse {
0%, 100% {
opacity: 0.82;
transform: scale(0.9);
}
50% {
opacity: 1;
transform: scale(1);
}
} }
.earth-tooltip { .earth-tooltip {

View File

@@ -26,13 +26,23 @@
.hud-panel-brand { .hud-panel-brand {
--brand-scale: 0.88; --brand-scale: 0.88;
--brand-copy-width: 160px; --brand-copy-width: 160px;
border-radius: 0; padding: calc(10px * var(--hud-scale)) calc(4px * var(--hud-scale)) calc(12px * var(--hud-scale)) 0;
padding: calc(18px * var(--hud-scale)) calc(20px * var(--hud-scale));
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: flex-start;
/* Reserve full panel height before brand images load */ /* Reserve full panel height before brand images load */
min-height: calc(66px * var(--hud-scale)); min-height: calc(66px * var(--hud-scale));
background: transparent;
border: 0;
box-shadow: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
isolation: isolate;
}
.hud-panel-brand::before,
.hud-panel-brand::after {
display: none;
} }
.hud-panel-brand .earth-brand { .hud-panel-brand .earth-brand {
@@ -40,6 +50,20 @@
align-items: center; align-items: center;
gap: calc(10px * var(--hud-scale) * var(--brand-scale)); gap: calc(10px * var(--hud-scale) * var(--brand-scale));
min-width: 0; min-width: 0;
position: relative;
}
.hud-panel-brand .earth-brand::after {
content: "";
position: absolute;
inset: calc(4px * var(--hud-scale)) calc(-10px * var(--hud-scale)) calc(6px * var(--hud-scale)) calc(-10px * var(--hud-scale));
background:
radial-gradient(circle at 18% 50%, rgba(145, 186, 255, 0.14), transparent 32%),
linear-gradient(90deg, rgba(145, 186, 255, 0.05), transparent 58%);
opacity: 0.75;
pointer-events: none;
filter: blur(14px);
z-index: -1;
} }
.hud-panel-brand .earth-brand__logo { .hud-panel-brand .earth-brand__logo {

View File

@@ -397,12 +397,7 @@
<div class="tv-panel-edge" data-edge="bl"></div> <div class="tv-panel-edge" data-edge="bl"></div>
</div> </div>
<div id="loading" class="earth-loading"> <div id="status-message" class="earth-status-message" aria-live="polite" aria-atomic="true"></div>
<div id="loading-spinner" class="earth-loading-spinner"></div>
<div id="loading-title" class="earth-loading-title earth-loading-text">正在初始化全球态势数据...</div>
<div id="loading-subtitle" class="earth-loading-subtitle">同步卫星、海底光缆、登陆点与BGP态势数据</div>
</div>
<div id="status-message" class="earth-status-message"></div>
<div id="tooltip" class="earth-tooltip"></div> <div id="tooltip" class="earth-tooltip"></div>
<div id="settings-modal" class="earth-settings-modal" aria-hidden="true"> <div id="settings-modal" class="earth-settings-modal" aria-hidden="true">
<div id="settings-backdrop" class="earth-settings-backdrop"></div> <div id="settings-backdrop" class="earth-settings-backdrop"></div>

View File

@@ -1386,21 +1386,17 @@ export function updateBGPVisualState(lockedObjectType, lockedObject, camera) {
if (isLocked) { if (isLocked) {
scale *= 1.1 + 0.14 * pulse; scale *= 1.1 + 0.14 * pulse;
opacity = BGP_CONFIG.opacity.collectorHover; opacity = 0.96;
haloOpacity = 0.022; haloOpacity = 0.05;
pulseOpacity = 0.012; pulseOpacity = 0.024;
coverageOpacity = 0.02; coverageOpacity = 0.036;
markerColor = blendHexColors( markerColor = 0xcff2ff;
BGP_CONFIG.collectorIcon.lockedNeutralColor,
marker.userData.baseColor || BGP_CONFIG.collectorColor,
BGP_CONFIG.collectorIcon.lockedBlend,
);
} else if (isHovered) { } else if (isHovered) {
scale *= 1.08; scale *= 1.08;
opacity = BGP_CONFIG.opacity.collectorHover; opacity = 0.88;
haloOpacity = 0.016; haloOpacity = 0.03;
pulseOpacity = 0.008; pulseOpacity = 0.014;
coverageOpacity = 0.014; coverageOpacity = 0.02;
markerColor = blendHexColors( markerColor = blendHexColors(
BGP_CONFIG.collectorIcon.hoverNeutralColor, BGP_CONFIG.collectorIcon.hoverNeutralColor,
marker.userData.baseColor || BGP_CONFIG.collectorColor, marker.userData.baseColor || BGP_CONFIG.collectorColor,
@@ -1482,12 +1478,13 @@ export function updateBGPVisualState(lockedObjectType, lockedObject, camera) {
if (isLocked || isLinkedCollectorLocked) { if (isLocked || isLinkedCollectorLocked) {
scale *= 1 + BGP_CONFIG.pulse.lockedAmplitude * pulse; scale *= 1 + BGP_CONFIG.pulse.lockedAmplitude * pulse;
opacity = opacity =
BGP_CONFIG.opacity.lockedMin + 0.9 +
(BGP_CONFIG.opacity.lockedMax - BGP_CONFIG.opacity.lockedMin) * pulse; 0.1 * pulse;
markerColor = 0xfff1a8;
ringBaseOpacity *= 1.2; ringBaseOpacity *= 1.2;
} else if (isHovered) { } else if (isHovered) {
scale *= BGP_CONFIG.marker.hoverScale; scale *= BGP_CONFIG.marker.hoverScale;
opacity = BGP_CONFIG.opacity.hover; opacity = 0.9;
ringBaseOpacity *= 1.05; ringBaseOpacity *= 1.05;
} else if (isOtherLocked) { } else if (isOtherLocked) {
scale *= BGP_CONFIG.marker.dimmedScale; scale *= BGP_CONFIG.marker.dimmedScale;

View File

@@ -558,14 +558,18 @@ export function applyLandingPointVisualState(lockedCableName, dimAll = false, ca
lp.userData.cableNames.some((name) => relatedNames.includes(name)); lp.userData.cableNames.some((name) => relatedNames.includes(name));
if (isRelated) { if (isRelated) {
lp.material.color.setHex(CABLE_CONFIG.landingPoint.color); lp.material.color.setHex(0xffd27a);
lp.material.emissive.setHex(CABLE_CONFIG.landingPoint.emissive); lp.material.emissive.setHex(0x7a4a00);
lp.material.emissiveIntensity = lp.material.emissiveIntensity =
CABLE_CONFIG.landingPointVisual.related.emissiveIntensityBase + CABLE_CONFIG.landingPointVisual.related.emissiveIntensityBase +
pulse * CABLE_CONFIG.landingPointVisual.related.emissiveIntensityPulse; 0.2 +
pulse * (CABLE_CONFIG.landingPointVisual.related.emissiveIntensityPulse + 0.2);
lp.material.opacity = lp.material.opacity =
CABLE_CONFIG.landingPointVisual.related.opacityBase + Math.max(
pulse * CABLE_CONFIG.landingPointVisual.related.opacityPulse; 0.92,
CABLE_CONFIG.landingPointVisual.related.opacityBase +
pulse * CABLE_CONFIG.landingPointVisual.related.opacityPulse,
);
const distanceScale = getLandingPointDistanceScale(lp, camera); const distanceScale = getLandingPointDistanceScale(lp, camera);
const baseScale = lp.userData?.baseScale || CABLE_CONFIG.landingPoint.baseScale; const baseScale = lp.userData?.baseScale || CABLE_CONFIG.landingPoint.baseScale;
lp.scale.setScalar( lp.scale.setScalar(

View File

@@ -68,9 +68,9 @@ export const CELESTIAL_CONFIG = {
brightStarMaxScale: 6.8, brightStarMaxScale: 6.8,
brightStarOpacity: 0.88, brightStarOpacity: 0.88,
sunLightDistance: 460, sunLightDistance: 460,
sunLightIntensity: 1.28, sunLightIntensity: 1.02,
sunLightColor: 0xfff4df, sunLightColor: 0xfff4df,
backLightIntensity: 0.18, backLightIntensity: 0.3,
backLightColor: 0x2b4c78, backLightColor: 0x2b4c78,
}; };
@@ -300,12 +300,12 @@ export const EARTH_MATERIAL_CONFIG = {
dayNight: { dayNight: {
enabled: true, enabled: true,
sunDirection: { x: 1, y: 0.2, z: 0.4 }, sunDirection: { x: 1, y: 0.2, z: 0.4 },
nightFloor: 0.18, nightFloor: 0.32,
dayBoost: 1.08, dayBoost: 0.94,
twilightWidth: 0.18, twilightWidth: 0.24,
twilightIntensity: 0.22, twilightIntensity: 0.14,
twilightColor: 0x4ea0ff, twilightColor: 0x4ea0ff,
nightTintColor: 0x0b1830, nightTintColor: 0x0b1830,
nightTintIntensity: 0.1, nightTintIntensity: 0.05,
}, },
}; };

View File

@@ -700,14 +700,17 @@ function applyCableVisualState() {
switch (state) { switch (state) {
case CABLE_STATE.LOCKED: case CABLE_STATE.LOCKED:
cable.material.opacity = cable.material.opacity =
CABLE_CONFIG.lockedOpacityMin + Math.max(
pulse * 0.92,
(CABLE_CONFIG.lockedOpacityMax - CABLE_CONFIG.lockedOpacityMin); CABLE_CONFIG.lockedOpacityMin +
cable.material.color.setRGB(1, 1, 1); pulse *
(CABLE_CONFIG.lockedOpacityMax - CABLE_CONFIG.lockedOpacityMin),
);
cable.material.color.setRGB(0.86, 0.96, 1.0);
break; break;
case CABLE_STATE.HOVERED: case CABLE_STATE.HOVERED:
cable.material.opacity = 1; cable.material.opacity = 1;
cable.material.color.setRGB(1, 1, 1); cable.material.color.setRGB(0.92, 0.98, 1.0);
break; break;
case CABLE_STATE.NORMAL: case CABLE_STATE.NORMAL:
default: default:
@@ -1039,7 +1042,7 @@ async function loadData() {
clearCableData(earth); clearCableData(earth);
clearSatelliteData(); clearSatelliteData();
setLoadingMessage("正在初始化...", "清除旧数据"); setLoadingMessage("正在初始化...");
setLoading(true); setLoading(true);
await yieldFrame(); await yieldFrame();
if (loadToken !== currentLoadToken) { isDataLoading = false; return; } if (loadToken !== currentLoadToken) { isDataLoading = false; return; }
@@ -1048,7 +1051,7 @@ async function loadData() {
// Step 1 — Landing points // Step 1 — Landing points
if (cablesEnabled) { if (cablesEnabled) {
setLoadingMessage("正在加载登陆点...", "同步海底光缆登陆站数据"); setLoadingMessage("正在加载登陆点...");
await yieldFrame(30); await yieldFrame(30);
try { try {
await loadLandingPoints(scene, earth); await loadLandingPoints(scene, earth);
@@ -1061,7 +1064,7 @@ async function loadData() {
// Step 2 — Cables // Step 2 — Cables
if (cablesEnabled) { if (cablesEnabled) {
setLoadingMessage("正在加载海缆...", "同步海底光缆网络数据"); setLoadingMessage("正在加载海缆...");
await yieldFrame(30); await yieldFrame(30);
try { try {
const cableCount = await loadGeoJSONFromPath(scene, earth); const cableCount = await loadGeoJSONFromPath(scene, earth);
@@ -1080,7 +1083,7 @@ async function loadData() {
// Step 3 — Satellites // Step 3 — Satellites
if (satellitesEnabled) { if (satellitesEnabled) {
setLoadingMessage("正在加载卫星...", "同步在轨卫星轨道数据"); setLoadingMessage("正在加载卫星...");
await yieldFrame(30); await yieldFrame(30);
try { try {
clearSatelliteData(); clearSatelliteData();
@@ -1100,7 +1103,7 @@ async function loadData() {
} }
// Step 4 — BGP // Step 4 — BGP
setLoadingMessage("正在加载BGP态势...", "同步全球路由观测数据"); setLoadingMessage("正在加载BGP态势...");
await yieldFrame(30); await yieldFrame(30);
try { try {
const bgpResult = await loadBGPAnomalies(scene, earth); const bgpResult = await loadBGPAnomalies(scene, earth);
@@ -1115,7 +1118,7 @@ async function loadData() {
await yieldFrame(); await yieldFrame();
// Step 5 — Earth texture (loads last so data layers appear on the white sphere first) // Step 5 — Earth texture (loads last so data layers appear on the white sphere first)
setLoadingMessage("正在加载地球纹理...", "加载8K卫星地图"); setLoadingMessage("正在加载地球纹理...");
await yieldFrame(30); await yieldFrame(30);
try { try {
await loadEarthTexture(); await loadEarthTexture();
@@ -1127,19 +1130,10 @@ async function loadData() {
// Step 6 — Terrain (if enabled) // Step 6 — Terrain (if enabled)
if (getShowTerrain()) { if (getShowTerrain()) {
setLoadingMessage("正在渲染地形...", "生成地表高程数据"); setLoadingMessage("正在渲染地形...");
await yieldFrame(50); await yieldFrame(50);
} }
if (errors.length > 0) {
const errorMessage = buildLoadErrorMessage(errors);
showError(errorMessage);
showStatusMessage(errorMessage, "error");
} else {
hideError();
showStatusMessage("数据已加载", "success");
}
updateStatsSummary(); updateStatsSummary();
updateCableToggleUi(cablesEnabled); updateCableToggleUi(cablesEnabled);
updateSatelliteToggleUi(satellitesEnabled); updateSatelliteToggleUi(satellitesEnabled);
@@ -1149,6 +1143,15 @@ async function loadData() {
refreshLegend(); refreshLegend();
setLoading(false); setLoading(false);
isDataLoading = false; isDataLoading = false;
if (errors.length > 0) {
const errorMessage = buildLoadErrorMessage(errors);
showError(errorMessage);
showStatusMessage(errorMessage, "error");
} else {
hideError();
showStatusMessage("数据已加载", "success");
}
} }
const POSITION_UPDATE_FORCE_DELTA = 250; const POSITION_UPDATE_FORCE_DELTA = 250;
@@ -1170,7 +1173,7 @@ export async function setCablesEnabled(enabled) {
return 0; return 0;
} }
setLoadingMessage("正在加载线缆数据...", "重建海缆与登陆点对象"); setLoadingMessage("正在加载线缆数据...");
setLoading(true); setLoading(true);
hideError(); hideError();
@@ -1203,7 +1206,7 @@ export async function setSatellitesEnabled(enabled) {
return 0; return 0;
} }
setLoadingMessage("正在加载卫星数据...", "重建卫星点位与轨迹缓存"); setLoadingMessage("正在加载卫星数据...");
setLoading(true); setLoading(true);
hideError(); hideError();

View File

@@ -2,8 +2,13 @@
let statusTimeoutId = null; let statusTimeoutId = null;
let statusHideTimeoutId = null; let statusHideTimeoutId = null;
let statusReplayTimeoutId = null;
const STATUS_BASE_CLASS = "earth-status-message"; const STATUS_BASE_CLASS = "earth-status-message";
const STATUS_DISPLAY_MS = 3000;
const STATUS_FADE_MS = 280;
let statusQueue = [];
let statusBusy = false;
let loadingActive = false;
let loadingLockedWidth = 0;
function getElement(id) { function getElement(id) {
return document.getElementById(id); return document.getElementById(id);
@@ -19,53 +24,97 @@ function clearStatusTimers() {
clearTimeout(statusTimeoutId); clearTimeout(statusTimeoutId);
statusTimeoutId = null; statusTimeoutId = null;
} }
if (statusHideTimeoutId) { if (statusHideTimeoutId) {
clearTimeout(statusHideTimeoutId); clearTimeout(statusHideTimeoutId);
statusHideTimeoutId = null; statusHideTimeoutId = null;
} }
}
if (statusReplayTimeoutId) { function clearLoadingWidthLock(statusEl) {
clearTimeout(statusReplayTimeoutId); loadingLockedWidth = 0;
statusReplayTimeoutId = null; if (statusEl) {
statusEl.style.minWidth = "";
} }
} }
// Show status message function updateLoadingWidthLock(statusEl) {
export function showStatusMessage(message, type = "info") { if (!statusEl || !loadingActive) return;
const nextWidth = Math.ceil(statusEl.getBoundingClientRect().width || statusEl.scrollWidth || 0);
if (nextWidth <= 0) return;
loadingLockedWidth = Math.max(loadingLockedWidth, nextWidth);
statusEl.style.minWidth = `${loadingLockedWidth}px`;
}
function buildStatusContent(statusEl, message, type) {
statusEl.innerHTML = "";
const indicator = document.createElement("span");
indicator.className = "earth-status-indicator";
indicator.setAttribute("aria-hidden", "true");
const dotCount = type === "loading" ? 3 : 1;
for (let i = 0; i < dotCount; i++) {
const dot = document.createElement("span");
dot.className = "earth-status-dot";
indicator.appendChild(dot);
}
const text = document.createElement("span");
text.className = "earth-status-text";
text.textContent = message;
statusEl.appendChild(indicator);
statusEl.appendChild(text);
}
function hideStatusElement(statusEl, onHidden) {
statusEl.classList.remove("visible");
statusHideTimeoutId = setTimeout(() => {
if (!statusEl.classList.contains("visible")) {
setElementDisplay(statusEl, false);
statusEl.className = STATUS_BASE_CLASS;
statusEl.innerHTML = "";
}
statusHideTimeoutId = null;
if (typeof onHidden === "function") {
onHidden();
}
}, STATUS_FADE_MS);
}
function processStatusQueue() {
if (loadingActive || statusBusy || statusQueue.length === 0) return;
const next = statusQueue.shift();
if (!next) return;
startTransientStatus(next.message, next.type);
}
function startTransientStatus(message, type = "info") {
const statusEl = getElement("status-message"); const statusEl = getElement("status-message");
if (!statusEl) return; if (!statusEl) return;
clearStatusTimers(); clearStatusTimers();
statusBusy = true;
const startShow = () => { buildStatusContent(statusEl, message, type);
statusEl.textContent = message; statusEl.className = `${STATUS_BASE_CLASS} ${type}`;
statusEl.className = `${STATUS_BASE_CLASS} ${type}`; setElementDisplay(statusEl, true, "inline-flex");
setElementDisplay(statusEl, true); statusEl.offsetHeight;
statusEl.offsetHeight; statusEl.classList.add("visible");
statusEl.classList.add("visible");
statusTimeoutId = setTimeout(() => { statusTimeoutId = setTimeout(() => {
statusEl.classList.remove("visible"); hideStatusElement(statusEl, () => {
statusHideTimeoutId = setTimeout(() => { statusBusy = false;
setElementDisplay(statusEl, false); processStatusQueue();
statusEl.textContent = ""; });
statusHideTimeoutId = null; statusTimeoutId = null;
}, 280); }, STATUS_DISPLAY_MS);
statusTimeoutId = null; }
}, 3000);
};
if (statusEl.classList.contains("visible")) { // Show status message
statusEl.classList.remove("visible"); export function showStatusMessage(message, type = "info") {
statusReplayTimeoutId = setTimeout(() => { statusQueue.push({ message, type });
startShow(); processStatusQueue();
statusReplayTimeoutId = null;
}, 180);
return;
}
startShow();
} }
// Update coordinates display // Update coordinates display
@@ -120,23 +169,49 @@ export function updateEarthStats(stats) {
textureQualityEl.textContent = stats.textureQuality || "8K 卫星图"; textureQualityEl.textContent = stats.textureQuality || "8K 卫星图";
} }
// Show/hide loading // Show/hide loading via status message
export function setLoading(loading) { export function setLoading(loading) {
const loadingEl = getElement("loading"); const statusEl = getElement("status-message");
if (!loadingEl) return; if (!statusEl) return;
setElementDisplay(loadingEl, loading);
if (loading) {
clearStatusTimers();
loadingActive = true;
statusBusy = false;
clearLoadingWidthLock(statusEl);
buildStatusContent(statusEl, "正在加载...", "loading");
statusEl.className = `${STATUS_BASE_CLASS} loading`;
setElementDisplay(statusEl, true, "inline-flex");
statusEl.offsetHeight;
statusEl.classList.add("visible");
requestAnimationFrame(() => {
updateLoadingWidthLock(statusEl);
});
} else {
if (!statusEl.classList.contains("loading")) {
loadingActive = false;
clearLoadingWidthLock(statusEl);
processStatusQueue();
return;
}
loadingActive = false;
hideStatusElement(statusEl, () => {
clearLoadingWidthLock(statusEl);
statusBusy = false;
processStatusQueue();
});
}
} }
export function setLoadingMessage(title, subtitle = "") { export function setLoadingMessage(title) {
const titleEl = getElement("loading-title"); const statusEl = getElement("status-message");
const subtitleEl = getElement("loading-subtitle"); if (!statusEl || !statusEl.classList.contains("loading")) return;
const textEl = statusEl.querySelector(".earth-status-text");
if (titleEl) { if (textEl) {
titleEl.textContent = title; textEl.textContent = title;
} requestAnimationFrame(() => {
updateLoadingWidthLock(statusEl);
if (subtitleEl) { });
subtitleEl.textContent = subtitle;
} }
} }
@@ -177,12 +252,16 @@ export function hideError() {
export function clearUiState() { export function clearUiState() {
clearStatusTimers(); clearStatusTimers();
statusQueue = [];
statusBusy = false;
loadingActive = false;
const statusEl = getElement("status-message"); const statusEl = getElement("status-message");
if (statusEl) { if (statusEl) {
statusEl.className = STATUS_BASE_CLASS; statusEl.className = STATUS_BASE_CLASS;
setElementDisplay(statusEl, false); setElementDisplay(statusEl, false);
statusEl.textContent = ""; statusEl.innerHTML = "";
clearLoadingWidthLock(statusEl);
} }
hideTooltip(); hideTooltip();

View File

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

2
uv.lock generated
View File

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