release: bump version to 0.64.0
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / delivery (push) Has been cancelled
release / images (push) Has been cancelled

This commit is contained in:
linkong
2026-05-21 03:46:02 +08:00
parent a37d4b6289
commit 37e92e7572
21 changed files with 1418 additions and 65 deletions

View File

@@ -1441,6 +1441,11 @@
gap: 8px;
}
.earth-about-meta-list {
display: grid;
gap: 8px;
}
.earth-about-meta {
display: flex;
flex-direction: column;
@@ -1460,6 +1465,311 @@
line-height: 1.45;
}
.earth-oobe {
position: fixed;
inset: 0;
z-index: 12000;
display: grid;
place-items: start center;
padding-top: min(16vh, 150px);
color: rgba(235, 245, 255, 0.96);
pointer-events: auto;
}
.earth-oobe__scrim {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 50% 28%, rgba(73, 136, 220, 0.18), transparent 38%),
rgba(2, 7, 16, 0.58);
animation: earth-oobe-fade-in 180ms ease-out both;
}
.earth-oobe__panel {
position: relative;
z-index: 1;
width: min(620px, calc(100vw - 36px));
overflow: hidden;
border: 1px solid rgba(132, 176, 236, 0.32);
border-radius: 22px;
background:
linear-gradient(135deg, rgba(11, 24, 45, 0.88), rgba(7, 14, 27, 0.76)),
rgba(8, 18, 34, 0.7);
box-shadow:
0 28px 80px rgba(0, 0, 0, 0.42),
inset 0 1px 0 rgba(255, 255, 255, 0.14);
backdrop-filter: blur(22px) saturate(1.28);
padding: 28px;
transform-origin: 50% 40%;
animation: earth-oobe-panel-in 520ms cubic-bezier(0.19, 1, 0.22, 1) both;
}
.earth-oobe__panel::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background:
linear-gradient(90deg, transparent, rgba(125, 194, 255, 0.16), transparent),
radial-gradient(circle at 88% 0%, rgba(84, 142, 255, 0.2), transparent 30%);
opacity: 0.82;
}
.earth-oobe__scan {
position: absolute;
left: -35%;
top: 0;
width: 34%;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(135, 211, 255, 0.9), transparent);
box-shadow: 0 0 22px rgba(86, 176, 255, 0.65);
animation: earth-oobe-scan 2.8s ease-in-out infinite;
}
.earth-oobe__close {
position: absolute;
right: 16px;
top: 14px;
width: 32px;
height: 32px;
border: 1px solid rgba(151, 190, 238, 0.28);
border-radius: 10px;
background: rgba(12, 24, 42, 0.58);
color: rgba(226, 240, 255, 0.86);
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.28);
cursor: pointer;
}
.earth-oobe__eyebrow,
.earth-oobe h2,
.earth-oobe__subtitle,
.earth-oobe__steps,
.earth-oobe__stats,
.earth-oobe__actions {
position: relative;
z-index: 1;
}
.earth-oobe__eyebrow {
color: rgba(125, 204, 255, 0.82);
font-size: 0.72rem;
font-weight: 900;
letter-spacing: 0.18em;
text-transform: uppercase;
}
.earth-oobe h2 {
margin: 10px 0;
color: #f7fbff;
font-size: clamp(1.7rem, 3vw, 2.4rem);
line-height: 1.05;
}
.earth-oobe__subtitle {
max-width: 520px;
margin: 0;
color: rgba(190, 211, 235, 0.88);
line-height: 1.7;
}
.earth-oobe__steps {
display: grid;
gap: 10px;
margin: 22px 0;
padding: 0;
list-style: none;
}
.earth-oobe__step {
display: flex;
align-items: center;
gap: 10px;
min-height: 28px;
color: rgba(222, 235, 250, 0.9);
animation: earth-oobe-step-in 360ms ease-out both;
animation-delay: calc(180ms + var(--step-index, 0) * 80ms);
}
.earth-oobe__step-dot {
width: 10px;
height: 10px;
border-radius: 999px;
background: #4ca3ff;
box-shadow: 0 0 18px rgba(76, 163, 255, 0.72);
animation: earth-oobe-dot-pulse 2.4s ease-in-out infinite;
}
.earth-oobe__step--done .earth-oobe__step-dot {
background: #36d399;
box-shadow: 0 0 18px rgba(54, 211, 153, 0.68);
}
.earth-oobe__step--warn .earth-oobe__step-dot {
background: #f6b73c;
box-shadow: 0 0 18px rgba(246, 183, 60, 0.62);
}
.earth-oobe__stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.earth-oobe__stats span {
border: 1px solid rgba(142, 178, 225, 0.22);
border-radius: 12px;
background: rgba(7, 15, 29, 0.46);
padding: 10px 12px;
color: rgba(180, 203, 229, 0.82);
font-size: 0.82rem;
}
.earth-oobe__stats strong {
display: block;
color: #ffffff;
font-size: 1.28rem;
line-height: 1.1;
}
.earth-oobe__actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 22px;
}
.earth-oobe__primary,
.earth-oobe__secondary,
.earth-oobe__ghost {
min-height: 38px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 12px;
padding: 0 16px;
font: inherit;
font-weight: 850;
text-decoration: none;
cursor: pointer;
}
.earth-oobe__primary {
border: 1px solid rgba(88, 164, 255, 0.8);
background: linear-gradient(180deg, #4e9eff, #1f67e8);
color: #ffffff;
box-shadow: 0 12px 28px rgba(30, 103, 232, 0.34);
}
.earth-oobe__secondary,
.earth-oobe__ghost {
border: 1px solid rgba(151, 190, 238, 0.28);
background: rgba(12, 24, 42, 0.52);
color: rgba(226, 240, 255, 0.9);
}
.earth-oobe--closing {
pointer-events: none;
animation: earth-oobe-fade-out 220ms ease-in both;
}
@keyframes earth-oobe-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes earth-oobe-fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes earth-oobe-panel-in {
from {
opacity: 0;
transform: translateY(26px) scale(0.96);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes earth-oobe-step-in {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes earth-oobe-scan {
0%, 34% { transform: translateX(0); opacity: 0; }
42% { opacity: 1; }
72%, 100% { transform: translateX(420%); opacity: 0; }
}
@keyframes earth-oobe-dot-pulse {
0%, 100% { transform: scale(1); opacity: 0.84; }
50% { transform: scale(1.18); opacity: 1; }
}
@media (max-width: 820px) {
.earth-oobe {
place-items: end center;
padding: 0 12px 14px;
}
.earth-oobe__panel {
width: min(100%, 520px);
max-height: 72vh;
overflow: auto;
border-radius: 22px 22px 18px 18px;
padding: 24px 20px;
animation-name: earth-oobe-sheet-in;
}
.earth-oobe__panel::after {
content: "";
position: absolute;
top: 9px;
left: 50%;
width: 42px;
height: 4px;
border-radius: 999px;
background: rgba(202, 222, 247, 0.32);
transform: translateX(-50%);
}
.earth-oobe__stats,
.earth-oobe__actions {
grid-template-columns: 1fr;
display: grid;
}
}
@keyframes earth-oobe-sheet-in {
from {
opacity: 0;
transform: translateY(34px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.earth-oobe,
.earth-oobe__panel,
.earth-oobe__scan,
.earth-oobe__step,
.earth-oobe__step-dot {
animation: none !important;
}
}
.earth-mobile-settings-card--stacked {
flex-direction: column;
align-items: stretch;

View File

@@ -0,0 +1,96 @@
const EARTH_ABOUT_API = "/api/v1/earth/about";
const DEFAULT_ABOUT = {
logo_src: "./assets/brand/lim-logo.png",
kicker: "About",
title: "智能星球计划",
version: "v0.64.0",
description:
"面向临空场景下的智能媒体研究、全球态势感知与多源开放数据巡航,提供可视化观测、事件聚合与交互式探索能力。",
meta: [
{ label: "出品方", value: "浙江大学临空智能媒体研究院" },
{ label: "策划人", value: "黄柳青" },
{ label: "产品兼开发者", value: "钱坤、张鸽、齐鹏" },
],
};
function escapeHtml(value = "") {
return String(value)
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function escapeAttribute(value = "") {
return escapeHtml(value);
}
function normalizeAbout(payload) {
const about = payload && typeof payload === "object" ? payload : {};
const meta = Array.isArray(about.meta) ? about.meta : DEFAULT_ABOUT.meta;
return {
...DEFAULT_ABOUT,
...about,
logo_src: about.logo_src || DEFAULT_ABOUT.logo_src,
kicker: about.kicker || DEFAULT_ABOUT.kicker,
title: about.title || DEFAULT_ABOUT.title,
version: about.version || DEFAULT_ABOUT.version,
description: about.description || DEFAULT_ABOUT.description,
meta: meta
.map((item) => ({
label: String(item?.label || "").trim(),
value: String(item?.value || "").trim(),
}))
.filter((item) => item.label || item.value),
};
}
function renderAboutCard(about, isMobile = false) {
const metaClass = isMobile ? "earth-about-meta-list" : "earth-about-grid";
const metaMarkup = about.meta
.map(
(item) => `
<div class="earth-about-meta">
<span>${escapeHtml(item.label)}</span>
<strong>${escapeHtml(item.value)}</strong>
</div>
`,
)
.join("");
return `
<div class="earth-about-logo-frame">
<img class="earth-about-logo" src="${escapeAttribute(about.logo_src)}" alt="${escapeAttribute(about.title)} Logo">
</div>
<div class="earth-about-heading">
<span class="earth-about-kicker">${escapeHtml(about.kicker)}</span>
<strong>${escapeHtml(about.title)}</strong>
<span>${escapeHtml(about.version)}</span>
</div>
<p class="earth-about-copy">${escapeHtml(about.description)}</p>
<div class="${metaClass}">
${metaMarkup}
</div>
`.trim();
}
function applyAboutConfig(about) {
document.querySelectorAll(".earth-about-card").forEach((card) => {
const isMobile = card.classList.contains("earth-about-card--mobile");
card.innerHTML = renderAboutCard(about, isMobile);
});
}
export async function initEarthAbout() {
applyAboutConfig(normalizeAbout(DEFAULT_ABOUT));
try {
const response = await fetch(EARTH_ABOUT_API, { cache: "no-store" });
if (!response.ok) throw new Error(`Earth about request failed: ${response.status}`);
const payload = await response.json();
applyAboutConfig(normalizeAbout(payload?.about));
} catch (error) {
console.warn("Earth about config unavailable, using defaults.", error);
}
}

View File

@@ -261,6 +261,8 @@ import {
setLegendItems,
} from "./legend.js";
import { fetchEarthBrandConfig, mountBrand } from "./brand.js";
import { initEarthAbout } from "./about.js";
import { initEarthOobe } from "./oobe.js";
import { initTVPanel } from "./tv.js";
import { initNewsPanel, refreshEarthNews, updateNewsViewFocus } from "./news.js";
import { initSearchPanel } from "./search.js";
@@ -3737,6 +3739,8 @@ export function init() {
console.warn("Earth brand config unavailable, using defaults.", error);
});
initTVPanel();
initEarthAbout();
initEarthOobe();
initNewsPanel();
connectEarthUpdatesRealtime();
initSearchPanel({

View File

@@ -0,0 +1,126 @@
const EARTH_OOBE_STATUS_API = "/api/v1/earth/oobe-status";
const EARTH_OOBE_SKIP_KEY = "planet-earth-oobe-skip-until-v1";
const EARTH_OOBE_SKIP_MS = 6 * 60 * 60 * 1000;
function getAuthToken() {
try {
const raw = window.localStorage?.getItem("auth-storage");
if (!raw) return "";
const parsed = JSON.parse(raw);
return parsed?.state?.token || parsed?.token || "";
} catch {
return "";
}
}
function isTemporarilySkipped() {
try {
const value = Number(window.localStorage?.getItem(EARTH_OOBE_SKIP_KEY) || 0);
return Number.isFinite(value) && value > Date.now();
} catch {
return false;
}
}
function markTemporarilySkipped() {
try {
window.localStorage?.setItem(EARTH_OOBE_SKIP_KEY, String(Date.now() + EARTH_OOBE_SKIP_MS));
} catch {
// Local storage is only a soft "remind later" hint.
}
}
function escapeAttribute(value = "") {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
async function fetchOobeStatus() {
const token = getAuthToken();
const headers = token ? { Authorization: `Bearer ${token}` } : {};
const response = await fetch(EARTH_OOBE_STATUS_API, { cache: "no-store", headers });
if (!response.ok) throw new Error(`Earth OOBE status request failed: ${response.status}`);
return response.json();
}
function statusSteps(status) {
return [
{ label: "后端服务在线", done: true },
{ label: "登录控制台", done: Boolean(status.authenticated), warn: !status.authenticated },
{ label: "配置或确认数据源", done: Number(status.datasource_count || 0) > 0 || Number(status.custom_config_count || 0) > 0 },
{ label: "触发首次采集", done: Boolean(status.has_collected_data), warn: !status.has_collected_data },
{ label: "回到 Earth 查看结果", done: Boolean(status.ready) },
];
}
function renderOobe(status) {
const authenticated = Boolean(status.authenticated);
const primaryHref = authenticated ? status.datasources_url || "/datasources" : status.login_url || "/login?next=/datasources";
const primaryText = authenticated ? "去采集数据" : "登录并采集数据";
const secondaryHref = status.collection_url || "/collection-management";
const subtitle = authenticated
? "当前还没有检测到可展示的数据,可以直接进入后台触发首次采集。"
: "登录控制台并完成首次采集后Earth 将显示实时数据层。";
const steps = statusSteps(status)
.map((step, index) => {
const state = step.done ? "done" : step.warn ? "warn" : "pending";
return `
<li class="earth-oobe__step earth-oobe__step--${state}" style="--step-index: ${index}">
<span class="earth-oobe__step-dot" aria-hidden="true"></span>
<span>${step.label}</span>
</li>
`;
})
.join("");
const root = document.createElement("div");
root.className = "earth-oobe";
root.setAttribute("role", "dialog");
root.setAttribute("aria-modal", "true");
root.setAttribute("aria-labelledby", "earth-oobe-title");
root.innerHTML = `
<div class="earth-oobe__scrim" data-oobe-close></div>
<section class="earth-oobe__panel">
<div class="earth-oobe__scan" aria-hidden="true"></div>
<button type="button" class="earth-oobe__close" aria-label="先浏览 Earth" data-oobe-close>×</button>
<div class="earth-oobe__eyebrow">SYSTEM INITIALIZATION</div>
<h2 id="earth-oobe-title">欢迎使用智能星球计划</h2>
<p class="earth-oobe__subtitle">${subtitle}</p>
<ul class="earth-oobe__steps">${steps}</ul>
<div class="earth-oobe__stats">
<span><strong>${Number(status.current_record_count || 0)}</strong> 当前记录</span>
<span><strong>${Number(status.tv_source_count || 0)}</strong> 直播源</span>
<span><strong>${Number(status.active_datasource_count || 0)}</strong> 活跃数据源</span>
</div>
<div class="earth-oobe__actions">
<a class="earth-oobe__primary" href="${escapeAttribute(primaryHref)}">${primaryText}</a>
${authenticated ? `<a class="earth-oobe__secondary" href="${escapeAttribute(secondaryHref)}">进入采集管理</a>` : ""}
<button type="button" class="earth-oobe__ghost" data-oobe-close>先浏览 Earth</button>
</div>
</section>
`;
root.querySelectorAll("[data-oobe-close]").forEach((element) => {
element.addEventListener("click", () => {
markTemporarilySkipped();
root.classList.add("earth-oobe--closing");
window.setTimeout(() => root.remove(), 220);
});
});
return root;
}
export async function initEarthOobe() {
try {
const status = await fetchOobeStatus();
if (status?.ready || isTemporarilySkipped()) return;
document.body.appendChild(renderOobe(status));
} catch (error) {
console.warn("Earth OOBE status unavailable.", error);
}
}