Files
planet/frontend/public/earth/js/info-card.js
2026-04-21 18:35:40 +08:00

357 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// info-card.js - Unified info card module
import { showStatusMessage } from './ui.js';
let currentType = null;
let cardMounted = false;
const CARD_CONFIG = {
cable: {
icon: '🛥️',
title: '电缆详情',
className: 'cable',
fields: [
{ key: 'name', label: '名称' },
{ key: 'owner', label: '所有者' },
{ key: 'status', label: '状态' },
{ key: 'length', label: '长度' },
{ key: 'coords', label: '经纬度' },
{ key: 'rfs', label: '投入使用' }
]
},
satellite: {
icon: '🛰️',
title: '卫星详情',
className: 'satellite',
fields: [
{ key: 'name', label: '名称' },
{ key: 'norad_id', label: 'NORAD ID' },
{ key: 'inclination', label: '倾角', unit: '°' },
{ key: 'period', label: '周期', unit: '分钟' },
{ key: 'perigee', label: '近地点', unit: 'km' },
{ key: 'apogee', label: '远地点', unit: 'km' }
]
},
bgp: {
icon: '📡',
title: 'BGP事件详情',
className: 'bgp',
fields: [
{ key: 'anomaly_type', label: '事件类型' },
{ key: 'severity', label: '严重度' },
{ key: 'status', label: '状态' },
{ key: 'route_change', label: '事件特征' },
{ key: 'prefix', label: '前缀' },
{ key: 'as_path_display', label: '传播路径' },
{ key: 'origin_asn', label: '涉及 ASN' },
{ key: 'new_origin_asn', label: '关联 ASN' },
{ key: 'confidence', label: '置信度' },
{ key: 'collector', label: '主观测站' },
{ key: 'observed_by', label: '观测范围' },
{ key: 'impacted_scope', label: '影响区域' },
{ key: 'related_cables', label: '附近基础设施' },
{ key: 'related_satellites', label: '附近卫星' },
{ key: 'location', label: '观测位置' },
{ key: 'created_at', label: '事件时间' },
{ key: 'summary', label: '摘要' }
]
},
bgp_collector: {
icon: '📍',
title: 'BGP观测站详情',
className: 'bgp',
fields: [
{ key: 'collector', label: '采集器' },
{ key: 'location', label: '观测位置' },
{ key: 'anomaly_count', label: '当前事件数' },
{ key: 'observation_count', label: '观测事件数' },
{ key: 'recent_24h_observation_count', label: '近24h事件数' },
{ key: 'recent_7d_observation_count', label: '近7d事件数' },
{ key: 'prefix_count', label: '观测前缀数' },
{ key: 'origin_asn_count', label: '观测 ASN 数' },
{ key: 'top_event_types', label: '主要事件类型' },
{ key: 'coverage_halo', label: '日常活跃度' },
{ key: 'related_satellites', label: '附近卫星' },
{ key: 'latest_event_type', label: '最近事件类型' },
{ key: 'latest_observed_at', label: '最近活跃时间' },
{ key: 'baseline_scope', label: '日常覆盖范围' },
{ key: 'status', label: '状态' }
]
},
supercomputer: {
icon: '🖥️',
title: '超算详情',
className: 'supercomputer',
fields: [
{ key: 'name', label: '名称' },
{ key: 'rank', label: '排名' },
{ key: 'r_max', label: 'Rmax', unit: 'GFlops' },
{ key: 'r_peak', label: 'Rpeak', unit: 'GFlops' },
{ key: 'country', label: '国家' },
{ key: 'city', label: '城市' }
]
},
gpu_cluster: {
icon: '🎮',
title: 'GPU集群详情',
className: 'gpu_cluster',
fields: [
{ key: 'name', label: '名称' },
{ key: 'country', label: '国家' },
{ key: 'city', label: '城市' }
]
}
};
function getPanel() {
return document.getElementById('info-panel');
}
function setupInfoCardDrag(panel) {
const app = document.getElementById('container');
if (!app) return;
const handle = panel.querySelector('.hud-panel-drag-handle');
if (!handle) return;
let isDragging = false;
let startPointerX = 0;
let startPointerY = 0;
let startLeft = 0;
let startTop = 0;
const stopDragging = () => {
isDragging = false;
panel.classList.remove('is-dragging');
document.body.style.userSelect = '';
};
const onMove = (event) => {
if (!isDragging) return;
const appRect = app.getBoundingClientRect();
const panelRect = panel.getBoundingClientRect();
const nextLeft = Math.min(
Math.max(startLeft + (event.clientX - startPointerX), 0),
appRect.width - panelRect.width,
);
const nextTop = Math.min(
Math.max(startTop + (event.clientY - startPointerY), 0),
appRect.height - panelRect.height,
);
panel.style.left = `${nextLeft}px`;
panel.style.top = `${nextTop}px`;
};
handle.addEventListener('pointerdown', (event) => {
if (event.target.closest('.hud-panel-close, .info-card-close')) return;
isDragging = true;
startPointerX = event.clientX;
startPointerY = event.clientY;
const appRect = app.getBoundingClientRect();
const panelRect = panel.getBoundingClientRect();
startLeft = panelRect.left - appRect.left;
startTop = panelRect.top - appRect.top;
panel.style.left = `${startLeft}px`;
panel.style.top = `${startTop}px`;
panel.style.right = 'auto';
panel.style.bottom = 'auto';
panel.classList.add('is-dragging');
document.body.style.userSelect = 'none';
handle.setPointerCapture?.(event.pointerId);
});
handle.addEventListener('pointermove', onMove);
handle.addEventListener('pointerup', stopDragging);
handle.addEventListener('pointercancel', stopDragging);
handle.addEventListener('lostpointercapture', stopDragging);
}
function mountCard() {
if (cardMounted) return;
const container = document.getElementById('container');
if (!container) return;
const panel = document.createElement('div');
panel.id = 'info-panel';
panel.className = 'hud-panel hud-panel-info';
panel.setAttribute('aria-live', 'polite');
panel.innerHTML = `
<div id="info-card" class="info-card">
<div class="info-card-header hud-panel-drag-handle">
<span class="info-card-icon" id="info-card-icon">🛰️</span>
<h3 id="info-card-title">详情</h3>
<button class="info-card-close hud-panel-close" type="button" aria-label="关闭详情">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div id="info-card-content" class="info-card-content"></div>
</div>
`;
container.appendChild(panel);
const card = panel.querySelector('#info-card');
const content = panel.querySelector('#info-card-content');
// Prevent pointer events from reaching the earth canvas
const stopEvent = (event) => { event.stopPropagation(); };
[
'mousemove', 'mousedown', 'mouseup', 'click', 'dblclick', 'wheel',
'pointerdown', 'pointerup', 'pointermove',
'touchstart', 'touchmove', 'touchend',
].forEach((evt) => card.addEventListener(evt, stopEvent, { passive: false }));
// Close button
const closeBtn = card.querySelector('.info-card-close');
if (closeBtn) {
closeBtn.addEventListener('click', (event) => {
event.stopPropagation();
hideInfoCard();
});
}
// Copy value on label click
content.addEventListener('click', async (event) => {
const label = event.target.closest('.info-card-label');
if (!label) return;
const property = label.closest('.info-card-property');
const valueEl = property?.querySelector('.info-card-value');
const value = valueEl?.textContent?.trim();
if (!value || value === '-') {
showStatusMessage('无可复制内容', 'warning');
return;
}
try {
await navigator.clipboard.writeText(value);
showStatusMessage(`已复制${label.textContent}${value}`, 'success');
} catch (error) {
console.error('Copy failed:', error);
showStatusMessage('复制失败', 'error');
}
});
setupInfoCardDrag(panel);
cardMounted = true;
}
function positionPanel(panel, x, y, options = {}) {
if (!panel) return;
const margin = 12;
const offset = 14;
const vpW = window.innerWidth;
const vpH = window.innerHeight;
const scale = parseFloat(
getComputedStyle(document.documentElement).getPropertyValue('--hud-scale')
) || 1;
const estW = Math.min(300 * scale, vpW - 32);
const estH = Math.min(420 * scale, vpH * 0.7);
if (options.absolute === true) {
const clampedLeft = Math.min(
Math.max(margin, x),
Math.max(margin, vpW - estW - margin),
);
const clampedTop = Math.min(
Math.max(margin, y),
Math.max(margin, vpH - estH - margin),
);
panel.style.left = `${clampedLeft}px`;
panel.style.top = `${clampedTop}px`;
panel.style.right = 'auto';
panel.style.bottom = 'auto';
return;
}
let left = x + offset;
let top = y + offset;
if (left + estW > vpW - margin) left = x - estW - offset;
if (top + estH > vpH - margin) top = Math.max(margin, vpH - estH - margin);
panel.style.left = `${Math.max(margin, left)}px`;
panel.style.top = `${Math.max(margin, top)}px`;
panel.style.right = 'auto';
panel.style.bottom = 'auto';
}
function showPanel(x, y, options = {}) {
const panel = getPanel();
if (!panel) return;
if (x != null && y != null) positionPanel(panel, x, y, options);
panel.classList.add('is-visible');
}
function hidePanel() {
const panel = getPanel();
if (panel) panel.classList.remove('is-visible');
}
// No-op: event binding now happens lazily in mountCard()
export function initInfoCard() {}
export function setInfoCardNoBorder(noBorder = true) {
const card = document.getElementById('info-card');
if (card) {
card.classList.toggle('no-border', noBorder);
}
}
export function showInfoCard(type, data, options = {}) {
const config = CARD_CONFIG[type];
if (!config) {
console.warn('Unknown info card type:', type);
return;
}
mountCard();
currentType = type;
const card = document.getElementById('info-card');
const icon = document.getElementById('info-card-icon');
const title = document.getElementById('info-card-title');
const content = document.getElementById('info-card-content');
card.className = 'info-card ' + config.className;
icon.textContent = config.icon;
title.textContent = 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>
`;
}
content.innerHTML = html;
showPanel(options.x, options.y, options);
}
export function hideInfoCard() {
hidePanel();
currentType = null;
}
export function getCurrentType() {
return currentType;
}