release: bump version to 0.38.0
This commit is contained in:
@@ -3,6 +3,186 @@ import { showStatusMessage } from './ui.js';
|
||||
|
||||
let currentType = null;
|
||||
let cardMounted = false;
|
||||
let typewriterTimerId = null;
|
||||
let typewriterToken = 0;
|
||||
let pendingMobileDetailState = null;
|
||||
let mobileDetailsListenerBound = false;
|
||||
let renderedMobileDetailKey = null;
|
||||
|
||||
function getNewsSummaryText(data) {
|
||||
return (data?.summary || data?.title || '').trim() || '暂无摘要';
|
||||
}
|
||||
|
||||
function getNewsSummaryPreview(data, maxLength = 34) {
|
||||
const text = getNewsSummaryText(data).replace(/\s+/g, ' ').trim();
|
||||
if (text.length <= maxLength) return text;
|
||||
return `${text.slice(0, Math.max(0, maxLength - 1))}…`;
|
||||
}
|
||||
|
||||
function stopTypewriterAnimation() {
|
||||
typewriterToken += 1;
|
||||
if (typewriterTimerId) {
|
||||
window.clearTimeout(typewriterTimerId);
|
||||
typewriterTimerId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startTypewriterAnimation(target, text, options = {}) {
|
||||
if (!(target instanceof HTMLElement)) return;
|
||||
stopTypewriterAnimation();
|
||||
|
||||
const content = typeof text === 'string' ? text : '';
|
||||
const token = typewriterToken;
|
||||
const stepMs = Number.isFinite(options.stepMs) ? options.stepMs : 22;
|
||||
const startDelayMs = Number.isFinite(options.startDelayMs) ? options.startDelayMs : 90;
|
||||
|
||||
target.textContent = '';
|
||||
target.classList.add('is-typing');
|
||||
|
||||
let index = 0;
|
||||
const tick = () => {
|
||||
if (token !== typewriterToken) return;
|
||||
index += 1;
|
||||
target.textContent = content.slice(0, index);
|
||||
if (index < content.length) {
|
||||
typewriterTimerId = window.setTimeout(tick, stepMs);
|
||||
return;
|
||||
}
|
||||
target.classList.remove('is-typing');
|
||||
typewriterTimerId = null;
|
||||
};
|
||||
|
||||
typewriterTimerId = window.setTimeout(() => {
|
||||
if (token !== typewriterToken) return;
|
||||
if (!content) {
|
||||
target.classList.remove('is-typing');
|
||||
typewriterTimerId = null;
|
||||
return;
|
||||
}
|
||||
tick();
|
||||
}, startDelayMs);
|
||||
}
|
||||
|
||||
function renderNewsCardContent(content, data) {
|
||||
if (!(content instanceof HTMLElement)) return;
|
||||
const summary = getNewsSummaryText(data);
|
||||
content.innerHTML = `
|
||||
<div class="info-card-news-layout">
|
||||
<div class="info-card-news-kicker">NEWS SIGNAL</div>
|
||||
<div class="info-card-news-title">${data?.title || '新闻事件'}</div>
|
||||
<div class="info-card-news-summary-shell">
|
||||
<div class="info-card-news-summary-label">SUMMARY</div>
|
||||
<div class="info-card-news-summary" data-news-summary></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const summaryEl = content.querySelector('[data-news-summary]');
|
||||
startTypewriterAnimation(summaryEl, summary);
|
||||
}
|
||||
|
||||
function renderMobileNewsCardContent(content, data) {
|
||||
if (!(content instanceof HTMLElement)) return;
|
||||
const summary = getNewsSummaryText(data);
|
||||
content.innerHTML = `
|
||||
<div class="earth-mobile-news-detail">
|
||||
<div class="earth-mobile-news-detail-kicker">NEWS SIGNAL</div>
|
||||
<div class="earth-mobile-news-detail-title">${data?.title || '新闻事件'}</div>
|
||||
<div class="earth-mobile-news-detail-summary-shell">
|
||||
<div class="earth-mobile-news-detail-summary-label">SUMMARY</div>
|
||||
<div class="earth-mobile-news-detail-summary" data-news-summary></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const summaryEl = content.querySelector('[data-news-summary]');
|
||||
startTypewriterAnimation(summaryEl, summary, { stepMs: 20, startDelayMs: 70 });
|
||||
}
|
||||
|
||||
function renderMobileDetailContent(type, config, data) {
|
||||
const content = document.getElementById('mobile-info-card-content');
|
||||
if (!(content instanceof HTMLElement)) return;
|
||||
|
||||
stopTypewriterAnimation();
|
||||
if (type === 'news') {
|
||||
renderMobileNewsCardContent(content, data);
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const field of config.fields) {
|
||||
let value = data[field.key];
|
||||
if (value === undefined || value === null || value === '') {
|
||||
value = '-';
|
||||
} else if (typeof value === 'number') {
|
||||
value = value.toLocaleString();
|
||||
}
|
||||
if (field.unit && value !== '-') value = value + ' ' + field.unit;
|
||||
html += `
|
||||
<div class="earth-mobile-detail-row">
|
||||
<span class="earth-mobile-detail-row-label">${field.label}</span>
|
||||
<span class="earth-mobile-detail-row-value">${value}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
content.innerHTML = html;
|
||||
}
|
||||
|
||||
function getMobileDetailRenderKey(type, data) {
|
||||
if (type !== 'news') return null;
|
||||
return [
|
||||
type,
|
||||
data?.id ?? '',
|
||||
data?.url ?? '',
|
||||
data?.published_at ?? '',
|
||||
data?.title ?? '',
|
||||
].join('|');
|
||||
}
|
||||
|
||||
function ensureMobileDetailsListener() {
|
||||
if (mobileDetailsListenerBound) return;
|
||||
mobileDetailsListenerBound = true;
|
||||
|
||||
window.addEventListener('earth:open-details-tab', () => {
|
||||
if (!document.body.classList.contains('layout-mode-mobile')) return;
|
||||
if (!pendingMobileDetailState) return;
|
||||
const nextKey = getMobileDetailRenderKey(
|
||||
pendingMobileDetailState.type,
|
||||
pendingMobileDetailState.data,
|
||||
);
|
||||
if (nextKey && nextKey === renderedMobileDetailKey) return;
|
||||
renderMobileDetailContent(
|
||||
pendingMobileDetailState.type,
|
||||
pendingMobileDetailState.config,
|
||||
pendingMobileDetailState.data,
|
||||
);
|
||||
renderedMobileDetailKey = nextKey;
|
||||
});
|
||||
}
|
||||
|
||||
function renderDefaultCardContent(content, config, data) {
|
||||
let html = '';
|
||||
for (const field of config.fields) {
|
||||
let value = data[field.key];
|
||||
|
||||
if (value === undefined || value === null || value === '') {
|
||||
value = '-';
|
||||
} else if (typeof value === 'number') {
|
||||
value = value.toLocaleString();
|
||||
}
|
||||
|
||||
if (field.unit && value !== '-') {
|
||||
value = value + ' ' + field.unit;
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="info-card-property">
|
||||
<span class="info-card-label">${field.label}</span>
|
||||
<span class="info-card-value">${value}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
content.innerHTML = html;
|
||||
}
|
||||
|
||||
// ── Mobile popup ─────────────────────────────────────────────
|
||||
|
||||
@@ -12,6 +192,7 @@ function getMobilePopupTitle(type, data) {
|
||||
case 'landing_point': return data.name || '登陆点';
|
||||
case 'satellite': return data.name || '卫星';
|
||||
case 'bgp': return data.anomaly_type || 'BGP事件';
|
||||
case 'news': return data.title || '新闻事件';
|
||||
case 'bgp_collector': return data.collector || 'BGP观测站';
|
||||
case 'supercomputer': return data.name || '超算';
|
||||
case 'gpu_cluster': return data.name || 'GPU集群';
|
||||
@@ -25,6 +206,7 @@ function getMobilePopupSubtitle(type, data) {
|
||||
case 'landing_point': return data.country || '登陆点';
|
||||
case 'satellite': return data.norad_id ? `NORAD ${data.norad_id}` : '卫星';
|
||||
case 'bgp': return data.severity || 'BGP路由异常';
|
||||
case 'news': return getNewsSummaryPreview(data, 30) || '态势新闻';
|
||||
case 'bgp_collector': return data.location || 'BGP观测站';
|
||||
case 'supercomputer': return data.country || '超级计算机';
|
||||
case 'gpu_cluster': return data.country || 'GPU集群';
|
||||
@@ -280,6 +462,20 @@ const CARD_CONFIG = {
|
||||
{ key: 'summary', label: '摘要' }
|
||||
]
|
||||
},
|
||||
news: {
|
||||
icon: '📰',
|
||||
title: '新闻事件详情',
|
||||
className: 'news',
|
||||
fields: [
|
||||
{ key: 'source', label: '来源' },
|
||||
{ key: 'published_at_display', label: '发布时间' },
|
||||
{ key: 'location_label', label: '发生地' },
|
||||
{ key: 'region_label', label: '区域' },
|
||||
{ key: 'feed_name', label: '聚合源' },
|
||||
{ key: 'summary', label: '摘要' },
|
||||
{ key: 'url', label: '原文链接' }
|
||||
]
|
||||
},
|
||||
bgp_collector: {
|
||||
icon: '📍',
|
||||
title: 'BGP观测站详情',
|
||||
@@ -616,6 +812,8 @@ export function showInfoCard(type, data, options = {}) {
|
||||
|
||||
if (document.body.classList.contains('layout-mode-mobile')) {
|
||||
currentType = type;
|
||||
pendingMobileDetailState = { type, config, data };
|
||||
ensureMobileDetailsListener();
|
||||
|
||||
// Fill drawer details slot (accessible when user taps popup → opens details tab)
|
||||
const icon = document.getElementById('mobile-info-card-icon');
|
||||
@@ -624,27 +822,20 @@ export function showInfoCard(type, data, options = {}) {
|
||||
const content = document.getElementById('mobile-info-card-content');
|
||||
|
||||
if (icon) icon.textContent = config.icon;
|
||||
if (title) title.textContent = config.title;
|
||||
if (typeLabel) typeLabel.textContent = type.replaceAll('_', ' ');
|
||||
if (title) {
|
||||
title.textContent = type === 'news'
|
||||
? (data?.title || '新闻事件')
|
||||
: config.title;
|
||||
}
|
||||
if (typeLabel) {
|
||||
typeLabel.textContent = type === 'news'
|
||||
? 'news signal'
|
||||
: type.replaceAll('_', ' ');
|
||||
}
|
||||
|
||||
if (content) {
|
||||
let html = '';
|
||||
for (const field of config.fields) {
|
||||
let value = data[field.key];
|
||||
if (value === undefined || value === null || value === '') {
|
||||
value = '-';
|
||||
} else if (typeof value === 'number') {
|
||||
value = value.toLocaleString();
|
||||
}
|
||||
if (field.unit && value !== '-') value = value + ' ' + field.unit;
|
||||
html += `
|
||||
<div class="earth-mobile-detail-row">
|
||||
<span class="earth-mobile-detail-row-label">${field.label}</span>
|
||||
<span class="earth-mobile-detail-row-value">${value}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
content.innerHTML = html;
|
||||
if (content && type !== 'news') {
|
||||
renderMobileDetailContent(type, config, data);
|
||||
renderedMobileDetailKey = null;
|
||||
}
|
||||
|
||||
// Show the floating mini popup near the touch point (requires coordinates)
|
||||
@@ -667,37 +858,23 @@ export function showInfoCard(type, data, options = {}) {
|
||||
const title = document.getElementById('info-card-title');
|
||||
const content = document.getElementById('info-card-content');
|
||||
|
||||
stopTypewriterAnimation();
|
||||
card.className = 'info-card ' + config.className;
|
||||
icon.textContent = config.icon;
|
||||
title.textContent = config.title;
|
||||
title.textContent = type === 'news'
|
||||
? (data?.title || '新闻事件')
|
||||
: config.title;
|
||||
|
||||
let html = '';
|
||||
for (const field of config.fields) {
|
||||
let value = data[field.key];
|
||||
|
||||
if (value === undefined || value === null || value === '') {
|
||||
value = '-';
|
||||
} else if (typeof value === 'number') {
|
||||
value = value.toLocaleString();
|
||||
}
|
||||
|
||||
if (field.unit && value !== '-') {
|
||||
value = value + ' ' + field.unit;
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="info-card-property">
|
||||
<span class="info-card-label">${field.label}</span>
|
||||
<span class="info-card-value">${value}</span>
|
||||
</div>
|
||||
`;
|
||||
if (type === 'news') {
|
||||
renderNewsCardContent(content, data);
|
||||
} else {
|
||||
renderDefaultCardContent(content, config, data);
|
||||
}
|
||||
|
||||
content.innerHTML = html;
|
||||
showPanel(options.x, options.y, options);
|
||||
}
|
||||
|
||||
export function hideInfoCard() {
|
||||
stopTypewriterAnimation();
|
||||
if (document.body.classList.contains('layout-mode-mobile')) {
|
||||
hideMobilePopup();
|
||||
document.body.classList.remove('earth-info-open');
|
||||
@@ -705,6 +882,8 @@ export function hideInfoCard() {
|
||||
new CustomEvent('earth:info-card-visibility-change', { detail: { visible: false } })
|
||||
);
|
||||
currentType = null;
|
||||
pendingMobileDetailState = null;
|
||||
renderedMobileDetailKey = null;
|
||||
return;
|
||||
}
|
||||
hidePanel();
|
||||
|
||||
Reference in New Issue
Block a user