Files
KSP_project/data/wiki/guanghan_topology_demo.html
T

544 lines
29 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>广寒基地拓扑结构</title>
<style>
:root {
color-scheme: dark;
--bg: #070d18; --panel: #0d1728; --panel-2: #101c30;
--text: #eaf2ff; --muted: #91a3bd; --border: #263750;
--structural: #8fa1b8; --pressurized: #51e89c; --utility: #b178e6;
--core: #ffad42; --habitat: #52baff; --power: #d5b642;
--vehicle: #c28cdf; --radiator: #f0d869;
}
* { box-sizing: border-box; }
body {
margin: 0; min-height: 100vh; padding: 30px;
background: radial-gradient(circle at 50% 0%, rgba(51,88,140,0.18), transparent 36rem), var(--bg);
color: var(--text);
font-family: "Microsoft YaHei", "PingFang SC", system-ui, sans-serif;
}
.page { width: min(100%, 1500px); margin: 0 auto; }
.page-header { text-align: center; margin-bottom: 22px; }
h1 { margin: 0 0 7px; font-size: clamp(1.7rem, 3vw, 2.45rem); font-weight: 650; letter-spacing: 0.08em; }
.subtitle { margin: 0; color: var(--muted); }
.toolbar { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 12px; margin: 16px 0; }
.toolbar label { color: var(--muted); font-size: 0.88rem; }
.toolbar select, .toolbar input { padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--panel); color: var(--text); font-size: 0.88rem; }
.toolbar button { padding: 6px 16px; border: 1px solid var(--pressurized); border-radius: 6px; background: #0a2e16; color: var(--pressurized); cursor: pointer; font-size: 0.88rem; font-weight: 600; }
.toolbar button:hover { background: #0f3d1f; }
.loading-msg { text-align: center; color: var(--muted); padding: 40px; }
.error-msg { text-align: center; color: #f66; padding: 40px; }
.legend { display: flex; flex-wrap: wrap; justify-content: center; gap: 13px 24px; margin: 20px 0; padding: 13px 18px; border: 1px solid var(--border); border-radius: 12px; background: rgba(13,23,40,0.84); }
.legend-item { display: inline-flex; align-items: center; gap: 9px; color: #c8d5e7; font-size: 0.86rem; }
.legend-line { display: inline-block; width: 46px; border-top: 4px solid; }
.legend-line.structural { border-color: var(--structural); }
.legend-line.pressurized { border-color: var(--pressurized); }
.legend-line.utility { border-color: var(--utility); border-top-style: dashed; }
.legend-chip { width: 19px; height: 13px; border: 2px solid var(--radiator); background: #665511; border-radius: 3px; }
.map-shell { overflow: auto; border: 1px solid var(--border); border-radius: 18px; background: #08101e; box-shadow: 0 24px 70px rgba(0,0,0,0.34); }
.topology-map { display: block; width: 100%; min-width: 1080px; height: auto; background: linear-gradient(rgba(18,36,60,0.21) 1px, transparent 1px), linear-gradient(90deg, rgba(18,36,60,0.21) 1px, transparent 1px), #08101e; background-size: 48px 48px; }
.connection { fill: none; vector-effect: non-scaling-stroke; stroke-linecap: square; }
.connection.structural { stroke: var(--structural); stroke-width: 6; }
.connection.pressurized { stroke: var(--pressurized); stroke-width: 9; }
.connection.utility { stroke: var(--utility); stroke-width: 4; stroke-dasharray: 13 10; }
.topology-node { cursor: pointer; outline: none; }
.topology-node rect { vector-effect: non-scaling-stroke; transition: filter 150ms ease, stroke-width 150ms ease; }
.topology-node:hover rect, .topology-node:focus-visible rect { filter: brightness(1.25); stroke-width: 5; }
.topology-node.hidden { display: none; }
.node-card { fill: #10263a; stroke: var(--habitat); stroke-width: 3; }
.core-card { fill: #3b250d; stroke: var(--core); stroke-width: 5; }
.power-card { fill: #2a2810; stroke: var(--power); stroke-width: 3; }
.power-node-card { fill: #393412; stroke: var(--power); stroke-width: 5; }
.vehicle-card { fill: #291c34; stroke: var(--vehicle); stroke-width: 3; }
.node-title { fill: var(--text); font-size: 25px; font-weight: 700; text-anchor: middle; }
.node-meta { fill: #a6b5c9; font-size: 17px; text-anchor: middle; }
.node-hint { fill: #71849e; font-size: 14px; text-anchor: middle; }
.radiator { pointer-events: none; }
.radiator rect { fill: #6b5814; stroke: var(--radiator); stroke-width: 2; vector-effect: non-scaling-stroke; }
.radiator line { stroke: #f8e798; stroke-width: 1.5; vector-effect: non-scaling-stroke; }
.radiator-label { fill: #f4df7b; font-size: 15px; font-weight: 650; text-anchor: middle; }
.click-note { margin: 13px 0 0; color: var(--muted); font-size: 0.84rem; text-align: center; }
body.modal-open { overflow: hidden; }
.modal-backdrop { position: fixed; inset: 0; z-index: 20; display: none; align-items: center; justify-content: center; padding: 24px; background: rgba(2,7,14,0.82); backdrop-filter: blur(8px); }
.modal-backdrop[aria-hidden="false"] { display: flex; }
.detail-dialog { position: relative; width: min(760px, 100%); max-height: min(820px, calc(100vh - 48px)); overflow: auto; padding: 30px; border: 1px solid #405572; border-radius: 18px; outline: none; background: linear-gradient(145deg, #13233a, #0b1424 72%); box-shadow: 0 30px 100px rgba(0,0,0,0.64); }
.detail-dialog:focus-visible { box-shadow: 0 0 0 3px var(--pressurized), 0 30px 100px rgba(0,0,0,0.64); }
.modal-close { position: absolute; top: 14px; right: 14px; width: 40px; height: 40px; border: 1px solid #4e627e; border-radius: 50%; background: #182a43; color: var(--text); font: 500 1.65rem/1 system-ui, sans-serif; cursor: pointer; }
.modal-close:hover, .modal-close:focus-visible { border-color: var(--pressurized); outline: none; background: #203754; }
.detail-kicker { margin: 0 46px 6px 0; color: var(--pressurized); font-size: 0.77rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; }
#detail-title { margin: 0 50px 10px 0; font-size: 1.85rem; }
#detail-role { margin: 0 0 22px; color: #c4d2e4; line-height: 1.7; }
.detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin: 0; }
.detail-grid > div { min-width: 0; padding: 14px; border: 1px solid #293c57; border-radius: 10px; background: rgba(8,18,33,0.7); }
.detail-grid dt { margin-bottom: 7px; color: #7f94b1; font-size: 0.75rem; }
.detail-grid dd { margin: 0; color: #e1ebf8; line-height: 1.55; overflow-wrap: anywhere; }
.detail-grid dd span { display: block; }
.detail-grid dd span + span { margin-top: 4px; }
@media (max-width: 760px) {
body { padding: 14px; } .topology-map { min-width: 980px; } .legend { justify-content: flex-start; }
.modal-backdrop { padding: 12px; } .detail-dialog { padding: 25px 18px 20px; max-height: calc(100vh - 24px); }
.detail-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<main class="page">
<header class="page-header">
<h1>广寒基地拓扑结构</h1>
<p class="subtitle" id="page-subtitle">加载中...</p>
</header>
<div class="toolbar">
<label for="mission-select">任务:</label>
<select id="mission-select">
<option value="">(最新)</option>
</select>
<label for="time-input">时间:</label>
<input type="text" id="time-input" placeholder="ISO 8601, 如 2024-09-01T00:00:00Z" size="28">
<button id="go-btn">查询</button>
<span style="color:var(--muted);font-size:0.82rem;" id="snapshot-info"></span>
</div>
<div class="legend" aria-label="图例">
<span class="legend-item"><i class="legend-line structural"></i>结构直连</span>
<span class="legend-item"><i class="legend-line pressurized"></i>加压走道</span>
<span class="legend-item"><i class="legend-line utility"></i>燃料 / 供电</span>
<span class="legend-item"><i class="legend-chip"></i>大型折叠散热面板</span>
</div>
<div id="loading-msg" class="loading-msg">正在从 API 加载拓扑数据...</div>
<div id="error-msg" class="error-msg" style="display:none;"></div>
<div class="map-shell" id="map-shell" style="display:none;">
<svg class="topology-map" viewBox="0 0 1500 1900" role="img" aria-labelledby="topology-title" id="topology-svg">
<title id="topology-title">广寒基地拓扑</title>
<desc id="topology-desc">核心舱四向安装四个舱段,南侧经居住舱一号连接电力模块十字结构。</desc>
<!-- Structural: core to modules (N/E/S/W) -->
<line class="connection structural" x1="750" y1="360" x2="750" y2="470" data-conn="core-structural" data-link="core_cabin.north-habitat_2.core"/>
<line class="connection structural" x1="550" y1="570" x2="630" y2="570" data-conn="core-structural" data-link="core_cabin.west-greenhouse.core"/>
<line class="connection structural" x1="870" y1="570" x2="950" y2="570" data-conn="core-structural" data-link="core_cabin.east-lab_module.core"/>
<line class="connection structural" x1="750" y1="670" x2="750" y2="760" data-conn="core-structural" data-link="core_cabin.south-habitat_1.core"/>
<!-- Pressurized: exactly two -->
<line class="connection pressurized" x1="250" y1="570" x2="330" y2="570" data-conn="pressurized" data-link="wugang_2.dock-greenhouse.distal"/>
<line class="connection pressurized" x1="750" y1="880" x2="750" y2="990" data-conn="pressurized" data-link="habitat_1.distal-power_module_hub.north"/>
<!-- Core cross nodes -->
<g class="topology-node" data-module="habitat-2" data-comp="habitat_2" role="button" tabindex="0">
<rect class="node-card" x="630" y="220" width="240" height="140" rx="16"/>
<text class="node-title" x="750" y="278">居住舱#2</text>
<text class="node-meta" x="750" y="315">核心舱北部安装口</text>
<text class="node-hint" x="750" y="340">加载中...</text>
</g>
<g class="topology-node" data-module="wugang-2" data-comp="wugang_2" role="button" tabindex="0">
<rect class="vehicle-card" x="40" y="495" width="210" height="150" rx="16"/>
<text class="node-title" x="145" y="554">吴刚二号</text>
<text class="node-meta" x="145" y="591">Block 2 载人着陆器</text>
<text class="node-hint" x="145" y="619">加载中...</text>
</g>
<g class="topology-node" data-module="greenhouse" data-comp="greenhouse" role="button" tabindex="0">
<rect class="node-card" x="330" y="490" width="220" height="160" rx="16"/>
<text class="node-title" x="440" y="548">温室模块</text>
<text class="node-meta" x="440" y="585">核心舱西部安装口</text>
<text class="node-hint" x="440" y="617">加载中...</text>
</g>
<g class="topology-node" data-module="core" data-comp="core_cabin" role="button" tabindex="0">
<rect class="core-card" x="630" y="470" width="240" height="200" rx="18"/>
<text class="node-title" x="750" y="545">广寒核心舱</text>
<text class="node-meta" x="750" y="585">四向安装节点</text>
<text class="node-hint" x="750" y="620">加载中...</text>
<text class="node-hint" x="750" y="646">加载中...</text>
</g>
<g class="topology-node" data-module="laboratory" data-comp="lab_module" role="button" tabindex="0">
<rect class="node-card" x="950" y="490" width="220" height="160" rx="16"/>
<text class="node-title" x="1060" y="548">实验舱</text>
<text class="node-meta" x="1060" y="585">核心舱东部安装口</text>
<text class="node-hint" x="1060" y="617">加载中...</text>
</g>
<g class="topology-node" data-module="habitat-1" data-comp="habitat_1" role="button" tabindex="0">
<rect class="node-card" x="630" y="760" width="240" height="120" rx="16"/>
<text class="node-title" x="750" y="808">居住舱#1</text>
<text class="node-meta" x="750" y="846">核心舱南部安装口</text>
</g>
<!-- Power structural connections -->
<line class="connection structural" x1="750" y1="1150" x2="750" y2="1260" data-conn="power-structural" data-link="power_module_hub.north-power_storage_rack.mount"/>
<line class="connection structural" x1="540" y1="1350" x2="650" y2="1350" data-conn="power-structural" data-link="power_module_hub.west"/>
<line class="connection structural" x1="850" y1="1350" x2="960" y2="1350" data-conn="power-structural" data-link="power_module_hub.east"/>
<line class="connection structural" x1="750" y1="1440" x2="750" y2="1540" data-conn="power-structural" data-link="power_module_hub.south-nuclear_reactor.mount"/>
<!-- Utility: binglun fuel/power -->
<line class="connection utility" x1="240" y1="1350" x2="280" y2="1350" data-conn="utility-west"/>
<line class="connection utility" x1="750" y1="1690" x2="750" y2="1740" data-conn="utility-south"/>
<line class="connection utility" x1="1220" y1="1350" x2="1260" y2="1350" data-conn="utility-east"/>
<!-- Radiators -->
<g class="radiator"><rect x="820" y="972" width="130" height="16" rx="3"/><line x1="850" y1="973" x2="850" y2="987"/><line x1="885" y1="973" x2="885" y2="987"/><line x1="920" y1="973" x2="920" y2="987"/><text class="radiator-label" x="885" y="964">散热器</text></g>
<g class="radiator"><rect x="345" y="1232" width="130" height="16" rx="3"/><line x1="375" y1="1233" x2="375" y2="1247"/><line x1="410" y1="1233" x2="410" y2="1247"/><line x1="445" y1="1233" x2="445" y2="1247"/><text class="radiator-label" x="410" y="1224">散热器</text></g>
<g class="radiator"><rect x="1025" y="1232" width="130" height="16" rx="3"/><line x1="1055" y1="1233" x2="1055" y2="1247"/><line x1="1090" y1="1233" x2="1090" y2="1247"/><line x1="1125" y1="1233" x2="1125" y2="1247"/><text class="radiator-label" x="1090" y="1224">散热器</text></g>
<g class="radiator"><rect x="790" y="1518" width="130" height="16" rx="3"/><line x1="820" y1="1519" x2="820" y2="1533"/><line x1="855" y1="1519" x2="855" y2="1533"/><line x1="890" y1="1519" x2="890" y2="1533"/><text class="radiator-label" x="855" y="1510">散热器</text></g>
<!-- Power cross nodes -->
<g class="topology-node" data-module="power-north" data-comp="power_storage_rack" role="button" tabindex="0">
<rect class="power-card" x="600" y="990" width="300" height="160" rx="16"/>
<text class="node-title" x="750" y="1044">北部模块</text>
<text class="node-meta" x="750" y="1082">电力模块存储支架</text>
<text class="node-hint" x="750" y="1115">加载中...</text>
</g>
<g class="topology-node" data-module="power-west" data-comp="power_west_group" role="button" tabindex="0">
<rect class="power-card" x="280" y="1250" width="260" height="200" rx="16"/>
<text class="node-title" x="410" y="1310">西部模块</text>
<text class="node-meta" x="410" y="1348">加载中...</text>
<text class="node-hint" x="410" y="1382">加载中...</text>
</g>
<g class="topology-node" data-module="power-node" data-comp="power_module_hub" role="button" tabindex="0">
<rect class="power-node-card" x="650" y="1260" width="200" height="180" rx="18"/>
<text class="node-title" x="750" y="1325">电力模块</text>
<text class="node-meta" x="750" y="1363">四向安装节点</text>
<text class="node-hint" x="750" y="1397">加载中...</text>
</g>
<g class="topology-node" data-module="power-east" data-comp="power_east_group" role="button" tabindex="0">
<rect class="power-card" x="960" y="1250" width="260" height="200" rx="16"/>
<text class="node-title" x="1090" y="1310">东部模块</text>
<text class="node-meta" x="1090" y="1348">加载中...</text>
<text class="node-hint" x="1090" y="1382">加载中...</text>
</g>
<g class="topology-node" data-module="power-south" data-comp="nuclear_reactor" role="button" tabindex="0">
<rect class="power-card" x="620" y="1540" width="260" height="150" rx="16"/>
<text class="node-title" x="750" y="1600">南部模块</text>
<text class="node-meta" x="750" y="1640">核反应堆</text>
<text class="node-hint" x="750" y="1668">加载中...</text>
</g>
<!-- Bingluns -->
<g class="topology-node" data-module="binglun-1" data-comp="binglun_1" role="button" tabindex="0">
<rect class="vehicle-card" x="30" y="1280" width="210" height="140" rx="16"/>
<text class="node-title" x="135" y="1335">冰轮一号</text>
<text class="node-meta" x="135" y="1372">加载中...</text>
<text class="node-hint" x="135" y="1400">加载中...</text>
</g>
<g class="topology-node" data-module="binglun-2" data-comp="binglun_2" role="button" tabindex="0">
<rect class="vehicle-card" x="645" y="1740" width="210" height="120" rx="16"/>
<text class="node-title" x="750" y="1790">冰轮二号</text>
<text class="node-meta" x="750" y="1828">加载中...</text>
</g>
<g class="topology-node" data-module="binglun-3" data-comp="binglun_3" role="button" tabindex="0">
<rect class="vehicle-card" x="1260" y="1280" width="210" height="140" rx="16"/>
<text class="node-title" x="1365" y="1335">冰轮三号</text>
<text class="node-meta" x="1365" y="1372">加载中...</text>
<text class="node-hint" x="1365" y="1400">加载中...</text>
</g>
</svg>
</div>
<p class="click-note">模块与车辆支持详情查看;走道、管线和散热面板仅用于表达拓扑关系。</p>
</main>
<div class="modal-backdrop" id="detail-modal" aria-hidden="true">
<section class="detail-dialog" role="dialog" aria-modal="true" aria-labelledby="detail-title" tabindex="-1">
<button class="modal-close" type="button" aria-label="关闭详情">×</button>
<p class="detail-kicker" id="detail-type"></p>
<h2 id="detail-title"></h2>
<p id="detail-role"></p>
<dl class="detail-grid">
<div><dt>安装位置</dt><dd id="detail-location"></dd></div>
<div><dt>运行状态</dt><dd id="detail-status"></dd></div>
<div><dt>关键参数</dt><dd id="detail-metrics"></dd></div>
<div><dt>尺寸</dt><dd id="detail-dimensions"></dd></div>
<div><dt>连接关系</dt><dd id="detail-connections"></dd></div>
<div><dt>任务节点</dt><dd id="detail-mission"></dd></div>
</dl>
</section>
</div>
<script>
const API_BASE = '';
let currentData = null;
// Group node members for composite SVG elements
const GROUP_MEMBERS = {
power_west_group: ['isru_module', 'mining_module', 'ore_fuel_storage'],
power_east_group: ['fuel_storage_mod', 'centrifuge_mod'],
};
const TYPE_LABELS = { module: '舱段模块', node: '结构节点', vehicle: '着陆器/车辆', equipment: '设备模块' };
async function fetchSnapshot(mission, at) {
const params = new URLSearchParams();
if (at) { params.set('at', at); }
else if (mission) { params.set('mission', mission); }
const url = `${API_BASE}/api/v1/bases/guanghan/snapshot?${params}`;
const res = await fetch(url);
if (!res.ok) {
const err = await res.json();
throw new Error(err.message || `HTTP ${res.status}`);
}
return res.json();
}
async function loadMissions() {
// Hardcoded mission list for the dropdown
return [
'BL-01','GHC-01','JC-01','BL-02','GHC-02','GHC-03','BL-03','GHC-04','GHC-05'
];
}
function getExistingComponentKeys() {
if (!currentData) return new Set();
return new Set(currentData.components.map(c => c.key));
}
function isGroupNodeVisible(groupKey) {
const members = GROUP_MEMBERS[groupKey];
if (!members) return true;
const existing = getExistingComponentKeys();
return members.some(m => existing.has(m));
}
function updateNodeVisibility() {
if (!currentData) return;
const existing = getExistingComponentKeys();
document.querySelectorAll('.topology-node[data-comp]').forEach(node => {
const compKey = node.dataset.comp;
let visible;
if (GROUP_MEMBERS[compKey]) {
visible = isGroupNodeVisible(compKey);
} else {
visible = existing.has(compKey);
}
node.classList.toggle('hidden', !visible);
});
// Hide/show connections
const connTypes = { 'core-structural': ['core_cabin'], 'pressurized': ['habitat_1', 'greenhouse', 'wugang_2'], 'power-structural': ['power_module_hub'], 'utility-west': ['binglun_1'], 'utility-south': ['binglun_2'], 'utility-east': ['binglun_3'] };
document.querySelectorAll('.connection[data-conn]').forEach(line => {
const conn = line.dataset.conn;
const needed = connTypes[conn] || [];
const visible = needed.length === 0 || needed.some(k => existing.has(k));
line.style.display = visible ? '' : 'none';
});
}
function updateNodeLabels() {
if (!currentData) return;
const compMap = {};
currentData.components.forEach(c => { compMap[c.key] = c; });
document.querySelectorAll('.topology-node[data-comp]').forEach(node => {
const compKey = node.dataset.comp;
let comp;
if (GROUP_MEMBERS[compKey]) {
const members = GROUP_MEMBERS[compKey].map(k => compMap[k]).filter(Boolean);
if (members.length > 0) comp = members[0];
} else {
comp = compMap[compKey];
}
if (!comp) return;
// Update hint texts
const hints = node.querySelectorAll('.node-hint');
if (hints.length > 0 && comp.summary) {
hints[0].textContent = comp.summary;
}
const states = comp.states || {};
if (states.operation === 'operational' && states.operationDetails && states.operationDetails.powerPercent) {
if (hints.length > 1) hints[1].textContent = `功率: ${states.operationDetails.powerPercent}%`;
}
// Update meta
const metas = node.querySelectorAll('.node-meta');
if (metas.length > 0 && comp.type) {
metas[0].textContent = TYPE_LABELS[comp.type] || comp.type;
}
});
}
function buildConnectionsForComponent(compKey) {
if (!currentData) return [];
const result = [];
currentData.connections.forEach(conn => {
const [fromComp, fromPort] = conn.fromPort.split('.');
const [toComp, toPort] = conn.toPort.split('.');
if (fromComp === compKey || toComp === compKey) {
const typeLabel = { structural_mount: '结构直连', pressurized_passage: '加压走道', fuel: '燃料管线', power: '供电管线', docking: '对接' }[conn.type] || conn.type;
const other = fromComp === compKey ? toComp : fromComp;
const otherComp = currentData.components.find(c => c.key === other);
const otherName = otherComp ? otherComp.name : other;
const dir = fromComp === compKey ? '→' : '←';
result.push(`${dir} ${otherName}${typeLabel}`);
}
});
return result;
}
function openModal(compKey) {
if (!currentData) return;
const compMap = {};
currentData.components.forEach(c => { compMap[c.key] = c; });
let comps = [];
if (GROUP_MEMBERS[compKey]) {
comps = GROUP_MEMBERS[compKey].map(k => compMap[k]).filter(Boolean);
} else if (compMap[compKey]) {
comps = [compMap[compKey]];
}
if (comps.length === 0) return;
const primary = comps[0];
const typeLabel = TYPE_LABELS[primary.type] || primary.type;
document.getElementById('detail-type').textContent = typeLabel;
document.getElementById('detail-title').textContent = primary.name;
document.getElementById('detail-role').textContent = primary.description || primary.summary || '暂无描述';
// Location
const ports = primary.ports || [];
const portInfo = ports.map(p => `${p.displayName}${p.direction || '—'}`).join(' · ');
document.getElementById('detail-location').textContent = portInfo || '资料未记录';
// Status
const states = primary.states || {};
const statusParts = [];
if (states.lifecycle) statusParts.push(states.lifecycle === 'installed' ? '已安装' : states.lifecycle);
if (states.operation) {
const opLabel = { operational: '运行中', standby: '待机', offline: '离线' }[states.operation] || states.operation;
statusParts.push(opLabel);
if (states.operationDetails && states.operationDetails.powerPercent) {
statusParts.push(`功率 ${states.operationDetails.powerPercent}%`);
}
}
document.getElementById('detail-status').textContent = statusParts.join(' · ') || '资料未记录';
// Metrics
const dims = primary.dimensions || {};
const props = primary.properties || {};
const metrics = [];
if (dims.massT) metrics.push(`质量: ${dims.massT} t`);
if (props.capacity) metrics.push(`铺位: ${props.capacity}`);
if (props.sleepBags) metrics.push(`睡袋: ${props.sleepBags}`);
if (props.zones) metrics.push(`温区: ${props.zones}`);
if (props.foldingArms) metrics.push(`折叠臂: ${props.foldingArms}`);
if (props.block) metrics.push(`构型: Block ${props.block}`);
if (props.dryMassT) metrics.push(`干重: ${props.dryMassT} t`);
if (props.batteryKwh) metrics.push(`蓄电池: ${props.batteryKwh} kWh`);
if (props.dockingNodes) metrics.push(`安装节点: ${props.dockingNodes}`);
if (metrics.length === 0 && comps.length > 1) {
comps.forEach(c => { if (c.summary) metrics.push(`${c.name}: ${c.summary}`); });
}
document.getElementById('detail-metrics').innerHTML = metrics.map(m => `<span>${m}</span>`).join('') || '<span>资料未记录</span>';
// Dimensions
const dimLines = [];
if (dims.lengthM) dimLines.push(`长: ${dims.lengthM} m`);
if (dims.widthM) dimLines.push(`宽: ${dims.widthM} m`);
if (dims.heightM) dimLines.push(`高: ${dims.heightM} m`);
if (dims.diameterM) dimLines.push(`直径: ${dims.diameterM} m`);
if (props.transportHeightM) dimLines.push(`运输: ${props.transportHeightM}×${props.transportLengthM}×${props.transportWidthM} m`);
document.getElementById('detail-dimensions').innerHTML = dimLines.map(l => `<span>${l}</span>`).join('') || '<span>资料未记录</span>';
// Connections
const conns = buildConnectionsForComponent(compKey);
if (comps.length > 1) {
comps.slice(1).forEach(c => {
buildConnectionsForComponent(c.key).forEach(cc => { if (!conns.includes(cc)) conns.push(cc); });
});
}
document.getElementById('detail-connections').innerHTML = conns.length > 0
? conns.map(c => `<span>${c}</span>`).join('')
: '<span>无连接记录</span>';
// Mission
document.getElementById('detail-mission').textContent = primary.detailUrl
? `详见 Wiki: ${primary.detailUrl}`
: '资料未记录';
// Show modal
const modal = document.getElementById('detail-modal');
modal.setAttribute('aria-hidden', 'false');
document.body.classList.add('modal-open');
modal.querySelector('.detail-dialog').focus();
}
function closeModal() {
const modal = document.getElementById('detail-modal');
modal.setAttribute('aria-hidden', 'true');
document.body.classList.remove('modal-open');
}
async function load(mission, at) {
document.getElementById('loading-msg').style.display = 'block';
document.getElementById('error-msg').style.display = 'none';
document.getElementById('map-shell').style.display = 'none';
try {
currentData = await fetchSnapshot(mission, at);
document.getElementById('loading-msg').style.display = 'none';
document.getElementById('map-shell').style.display = 'block';
const q = currentData.query;
const missionLabel = q.requestedMission || '(最新)';
document.getElementById('page-subtitle').textContent =
`${missionLabel} · 快照时间: ${new Date(q.snapshotAt).toLocaleString('zh-CN')} · ${currentData.components.length} 组件`;
document.getElementById('snapshot-info').textContent =
`快照: ${q.snapshotAt ? new Date(q.snapshotAt).toLocaleString('zh-CN') : '—'} · ${q.anchorSource}`;
updateNodeVisibility();
updateNodeLabels();
} catch (err) {
document.getElementById('loading-msg').style.display = 'none';
document.getElementById('error-msg').style.display = 'block';
document.getElementById('error-msg').textContent = `加载失败: ${err.message}`;
}
}
// Init
(async () => {
// Setup mission dropdown
const missions = await loadMissions();
const sel = document.getElementById('mission-select');
missions.forEach(m => { const o = document.createElement('option'); o.value = m; o.textContent = m; sel.appendChild(o); });
// Parse URL params
const urlParams = new URLSearchParams(window.location.search);
const urlMission = urlParams.get('mission');
const urlAt = urlParams.get('at');
if (urlMission) sel.value = urlMission;
if (urlAt) document.getElementById('time-input').value = urlAt;
// Load initial data
await load(urlMission || null, urlAt || null);
// Wire up UI
document.getElementById('go-btn').addEventListener('click', () => {
const m = document.getElementById('mission-select').value || null;
const t = document.getElementById('time-input').value || null;
// Update URL
const p = new URLSearchParams();
if (m) p.set('mission', m);
if (t) p.set('at', t);
const newUrl = `${window.location.pathname}${p.toString() ? '?' + p : ''}`;
window.history.pushState({}, '', newUrl);
load(m, t);
});
// Click handlers for topology nodes
document.querySelectorAll('.topology-node[data-comp]').forEach(node => {
node.addEventListener('click', () => openModal(node.dataset.comp));
node.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openModal(node.dataset.comp); } });
});
// Modal close
document.getElementById('detail-modal').addEventListener('click', (e) => {
if (e.target === e.currentTarget) closeModal();
});
document.querySelector('.modal-close').addEventListener('click', closeModal);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && document.getElementById('detail-modal').getAttribute('aria-hidden') === 'false') {
closeModal();
}
});
})();
</script>
</body>
</html>