fix(topology): repair controls navigation and details

This commit is contained in:
2026-07-09 22:08:42 +08:00
parent 6ff18623e0
commit 5a8a04c440
5 changed files with 225 additions and 28 deletions
+87 -27
View File
@@ -147,7 +147,24 @@
const API_BASE = '';
const SVG_NS = 'http://www.w3.org/2000/svg';
const TYPE_LABELS = { module: '舱段模块', node: '结构节点', vehicle: '着陆器/车辆', equipment: '设备模块' };
const ANCHOR_LABELS = {
explicit_time: '明确时间',
mission_latest_event: '任务末条记录',
latest_recorded_state: '最新已记录状态',
no_events: '暂无记录',
};
const TIME_FORMATTER = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
});
let currentData = null;
let lastModalTrigger = null;
function svgElement(name, attributes = {}) {
const element = document.createElementNS(SVG_NS, name);
@@ -160,17 +177,50 @@ function svgElement(name, attributes = {}) {
async function fetchSnapshot(mission, at) {
const params = new URLSearchParams();
if (at) params.set('at', at);
else if (mission) params.set('mission', mission);
if (mission) params.set('mission', mission);
const response = await fetch(`${API_BASE}/api/v1/bases/guanghan/snapshot?${params}`);
if (!response.ok) {
const error = await response.json();
let error;
try {
error = await response.json();
} catch {
error = { message: `HTTP ${response.status}` };
}
throw new Error(error.message || `HTTP ${response.status}`);
}
return response.json();
}
async function loadMissions() {
return ['BL-01', 'GHC-01', 'JC-01', 'BL-02', 'GHC-02', 'GHC-03', 'BL-03', 'GHC-04', 'GHC-05'];
const response = await fetch(`${API_BASE}/api/v1/bases/guanghan/missions`);
if (!response.ok) throw new Error(`任务列表加载失败(HTTP ${response.status}`);
return (await response.json()).missions;
}
function formatSnapshotTime(value) {
return value ? TIME_FORMATTER.format(new Date(value)) : '—';
}
function isValidTimeInput(value) {
if (!value) return true;
if (!/(Z|[+-]\d{2}:\d{2})$/.test(value)) return false;
return !Number.isNaN(Date.parse(value));
}
function setError(message) {
const error = document.getElementById('error-msg');
error.textContent = message;
error.style.display = message ? 'block' : 'none';
}
function setLines(elementId, lines, fallback) {
const container = document.getElementById(elementId);
const values = lines.length ? lines : [fallback];
container.replaceChildren(...values.map(value => {
const line = document.createElement('span');
line.textContent = value;
return line;
}));
}
function splitEndpoint(endpoint) {
@@ -337,11 +387,11 @@ function renderTopology() {
appendNodeText(group, node, 'node-title', title, node.y + node.height * (hasHint ? 0.34 : 0.42));
appendNodeText(group, node, 'node-meta', node.displayMeta, node.y + node.height * (hasHint ? 0.60 : 0.72));
appendNodeText(group, node, 'node-hint', node.displayHint, node.y + node.height * 0.82);
group.addEventListener('click', () => openModal(node.nodeKey));
group.addEventListener('click', () => openModal(node.nodeKey, group));
group.addEventListener('keydown', event => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
openModal(node.nodeKey);
openModal(node.nodeKey, group);
}
});
nodeLayer.appendChild(group);
@@ -381,7 +431,7 @@ function buildConnectionsForComponent(componentKey) {
return result;
}
function openModal(nodeKey) {
function openModal(nodeKey, trigger = null) {
const components = getNodeComponents(nodeKey);
if (components.length === 0) return;
const primary = components[0];
@@ -425,8 +475,7 @@ function openModal(nodeKey) {
if (values.length) metrics.push(`${component.name}${values.join('')}`);
else if (components.length > 1 && component.summary) metrics.push(`${component.name}${component.summary}`);
});
document.getElementById('detail-metrics').innerHTML =
metrics.map(value => `<span>${value}</span>`).join('') || '<span>资料未记录</span>';
setLines('detail-metrics', metrics, '资料未记录');
const dimensionLines = [];
components.forEach(component => {
@@ -438,8 +487,7 @@ function openModal(nodeKey) {
if (dimensions.diameterM) values.push(`直径 ${dimensions.diameterM} m`);
if (values.length) dimensionLines.push(`${component.name}${values.join('')}`);
});
document.getElementById('detail-dimensions').innerHTML =
dimensionLines.map(value => `<span>${value}</span>`).join('') || '<span>资料未记录</span>';
setLines('detail-dimensions', dimensionLines, '资料未记录');
const connections = [];
components.forEach(component => {
@@ -447,12 +495,14 @@ function openModal(nodeKey) {
if (!connections.includes(value)) connections.push(value);
});
});
document.getElementById('detail-connections').innerHTML =
connections.map(value => `<span>${value}</span>`).join('') || '<span>无连接记录</span>';
document.getElementById('detail-mission').textContent =
primary.detailUrl ? `详见 Wiki${primary.detailUrl}` : '资料未记录';
setLines('detail-connections', connections, '无连接记录');
const missions = components.map(component =>
`${component.name}${component.introducedMission} · ${formatSnapshotTime(component.introducedAt)}`
);
setLines('detail-mission', missions, '资料未记录');
const modal = document.getElementById('detail-modal');
lastModalTrigger = trigger;
modal.setAttribute('aria-hidden', 'false');
document.body.classList.add('modal-open');
modal.querySelector('.detail-dialog').focus();
@@ -461,11 +511,13 @@ function openModal(nodeKey) {
function closeModal() {
document.getElementById('detail-modal').setAttribute('aria-hidden', 'true');
document.body.classList.remove('modal-open');
if (lastModalTrigger?.isConnected) lastModalTrigger.focus();
lastModalTrigger = null;
}
async function load(mission, at) {
document.getElementById('loading-msg').style.display = 'block';
document.getElementById('error-msg').style.display = 'none';
setError('');
document.getElementById('map-shell').style.display = 'none';
try {
currentData = await fetchSnapshot(mission, at);
@@ -473,15 +525,14 @@ async function load(mission, at) {
const query = currentData.query;
const missionLabel = query.requestedMission || '(最新)';
document.getElementById('page-subtitle').textContent =
`${missionLabel} · 快照时间: ${new Date(query.snapshotAt).toLocaleString('zh-CN')} · ${currentData.components.length} 组件`;
`${missionLabel} · 快照时间: ${formatSnapshotTime(query.snapshotAt)} · ${currentData.components.length} 组件`;
document.getElementById('snapshot-info').textContent =
`快照: ${query.snapshotAt ? new Date(query.snapshotAt).toLocaleString('zh-CN') : '—'} · ${query.anchorSource}`;
`快照: ${formatSnapshotTime(query.snapshotAt)} · ${ANCHOR_LABELS[query.anchorSource] || query.anchorSource}`;
document.getElementById('loading-msg').style.display = 'none';
document.getElementById('map-shell').style.display = 'block';
} catch (error) {
document.getElementById('loading-msg').style.display = 'none';
document.getElementById('error-msg').style.display = 'block';
document.getElementById('error-msg').textContent = `加载失败: ${error.message}`;
setError(`加载失败: ${error.message}`);
}
}
@@ -490,27 +541,36 @@ async function load(mission, at) {
const missions = await loadMissions();
missions.forEach(mission => {
const option = document.createElement('option');
option.value = mission;
option.textContent = mission;
option.value = mission.code;
option.textContent = mission.code;
missionSelect.appendChild(option);
});
const urlParams = new URLSearchParams(window.location.search);
const urlMission = urlParams.get('mission');
const urlAt = urlParams.get('at');
if (urlMission) missionSelect.value = urlMission;
if (urlAt) document.getElementById('time-input').value = urlAt;
await load(urlMission || null, urlAt || null);
async function applyUrlState() {
const urlParams = new URLSearchParams(window.location.search);
const mission = urlParams.get('mission') || '';
const at = urlParams.get('at') || '';
missionSelect.value = mission;
document.getElementById('time-input').value = at;
await load(mission || null, at || null);
}
await applyUrlState();
document.getElementById('go-btn').addEventListener('click', () => {
const mission = missionSelect.value || null;
const at = document.getElementById('time-input').value || null;
if (!isValidTimeInput(at)) {
setError('请输入带时区的 ISO 8601 时间,例如 2025-01-02T08:00:00+08:00。');
return;
}
const params = new URLSearchParams();
if (mission) params.set('mission', mission);
if (at) params.set('at', at);
window.history.pushState({}, '', `${window.location.pathname}${params.size ? `?${params}` : ''}`);
load(mission, at);
});
window.addEventListener('popstate', applyUrlState);
document.getElementById('detail-modal').addEventListener('click', event => {
if (event.target === event.currentTarget) closeModal();