- Add diagram_layouts and diagram_nodes tables - Seed layout coordinates extracted from SVG - Extend snapshot API to return layout data - Rewrite demo page to fetch from API dynamically - Support ?mission= and ?at= URL parameters - Modal details populated from database component data - Show/hide nodes based on component existence at snapshot time Co-Authored-By: Claude <noreply@anthropic.com>
544 lines
29 KiB
HTML
544 lines
29 KiB
HTML
<!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 = 'http://localhost:5000';
|
||
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>
|