release: bump version to 0.64.0
This commit is contained in:
@@ -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;
|
||||
|
||||
96
frontend/public/earth/js/about.js
Normal file
96
frontend/public/earth/js/about.js
Normal 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, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
126
frontend/public/earth/js/oobe.js
Normal file
126
frontend/public/earth/js/oobe.js
Normal 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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user