Files
KSP_project/data/wiki/guanghan_topology_demo.html
T

588 lines
26 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; }
.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 class="snapshot-info" 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>
<!-- FUTURE_EXPANSION_ANCHOR:north -->
<!-- FUTURE_EXPANSION_ANCHOR:east -->
<!-- FUTURE_EXPANSION_ANCHOR:west -->
<g id="connection-layer"></g>
<g id="decoration-layer"></g>
<g id="node-layer"></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 = '';
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);
Object.entries(attributes).forEach(([key, value]) => {
if (value !== null && value !== undefined) element.setAttribute(key, value);
});
return element;
}
async function fetchSnapshot(mission, at) {
const params = new URLSearchParams();
if (at) params.set('at', at);
if (mission) params.set('mission', mission);
const response = await fetch(`${API_BASE}/api/v1/bases/guanghan/snapshot?${params}`);
if (!response.ok) {
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() {
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) {
const separator = endpoint.indexOf('.');
return separator === -1 ? endpoint : endpoint.slice(0, separator);
}
function connectionClass(type) {
if (type === 'structural_mount') return 'structural';
if (type === 'pressurized_passage') return 'pressurized';
return 'utility';
}
function edgePoint(source, target) {
const sourceCenter = {
x: source.x + source.width / 2,
y: source.y + source.height / 2,
};
const targetCenter = {
x: target.x + target.width / 2,
y: target.y + target.height / 2,
};
const dx = targetCenter.x - sourceCenter.x;
const dy = targetCenter.y - sourceCenter.y;
if (dx === 0 && dy === 0) return sourceCenter;
const xScale = dx === 0 ? Infinity : (source.width / 2) / Math.abs(dx);
const yScale = dy === 0 ? Infinity : (source.height / 2) / Math.abs(dy);
const scale = Math.min(xScale, yScale);
return {
x: sourceCenter.x + dx * scale,
y: sourceCenter.y + dy * scale,
};
}
function fitSvgText(textElement, fullText, maxWidth) {
textElement.textContent = fullText || '';
if (!fullText) return;
if (textElement.getComputedTextLength() <= maxWidth) return;
let fitted = fullText;
while (fitted.length > 1) {
fitted = fitted.slice(0, -1);
textElement.textContent = `${fitted}…`;
if (textElement.getComputedTextLength() <= maxWidth) break;
}
}
function appendNodeText(group, node, className, text, y) {
if (!text) return;
const element = svgElement('text', {
class: className,
x: node.x + node.width / 2,
y,
});
group.appendChild(element);
fitSvgText(element, text, node.width - 24);
}
function createRadiator(node) {
const group = svgElement('g', { class: 'radiator', 'aria-label': '大型折叠散热面板' });
const width = Math.min(130, node.width * 0.55);
const x = node.x + node.width * 0.55;
const y = node.y - 18;
group.appendChild(svgElement('rect', { x, y, width, height: 16, rx: 3 }));
for (let index = 1; index < 4; index += 1) {
const lineX = x + width * index / 4;
group.appendChild(svgElement('line', { x1: lineX, y1: y + 1, x2: lineX, y2: y + 15 }));
}
const label = svgElement('text', {
class: 'radiator-label',
x: x + width / 2,
y: y - 8,
});
label.textContent = '散热器';
group.appendChild(label);
return group;
}
function renderTopology() {
const payload = currentData.layout;
if (!payload || !payload.layout) throw new Error('拓扑布局资料未记录');
const svg = document.getElementById('topology-svg');
svg.setAttribute('viewBox', `0 0 ${payload.layout.canvasWidth} ${payload.layout.canvasHeight}`);
const connectionLayer = document.getElementById('connection-layer');
const decorationLayer = document.getElementById('decoration-layer');
const nodeLayer = document.getElementById('node-layer');
connectionLayer.replaceChildren();
decorationLayer.replaceChildren();
nodeLayer.replaceChildren();
const componentMap = new Map(currentData.components.map(component => [component.key, component]));
const visibleNodes = payload.nodes
.filter(node => node.memberKeys.some(key => componentMap.has(key)));
const nodeByKey = new Map(visibleNodes.map(node => [node.nodeKey, node]));
const nodeByComponent = new Map();
visibleNodes.forEach(node => {
node.memberKeys.forEach(key => {
if (componentMap.has(key)) nodeByComponent.set(key, node);
});
});
const visualConnections = new Map();
currentData.connections.forEach(connection => {
const fromComponent = splitEndpoint(connection.fromPort);
const toComponent = splitEndpoint(connection.toPort);
const fromNode = nodeByComponent.get(fromComponent);
const toNode = nodeByComponent.get(toComponent);
if (!fromNode || !toNode || fromNode.nodeKey === toNode.nodeKey) return;
const style = connectionClass(connection.type);
const pair = [fromNode.nodeKey, toNode.nodeKey].sort();
const key = `${pair[0]}|${pair[1]}|${style}`;
if (!visualConnections.has(key)) {
visualConnections.set(key, { fromNode, toNode, style, connection });
}
});
visualConnections.forEach(({ fromNode, toNode, style, connection }) => {
const start = edgePoint(fromNode, toNode);
const end = edgePoint(toNode, fromNode);
connectionLayer.appendChild(svgElement('line', {
class: `connection ${style}`,
x1: start.x,
y1: start.y,
x2: end.x,
y2: end.y,
'data-type': connection.type,
'data-from': fromNode.nodeKey,
'data-to': toNode.nodeKey,
}));
});
visibleNodes.forEach(node => {
if (node.decorations && node.decorations.radiator) {
decorationLayer.appendChild(createRadiator(node));
}
});
[...visibleNodes]
.sort((left, right) => left.zIndex - right.zIndex)
.forEach(node => {
const components = node.memberKeys.map(key => componentMap.get(key)).filter(Boolean);
const primary = components[0];
const title = node.labelOverride || primary.name;
const group = svgElement('g', {
class: 'topology-node',
'data-node-key': node.nodeKey,
'data-comp': node.nodeKey,
role: 'button',
tabindex: 0,
});
const fullTitle = svgElement('title');
fullTitle.textContent = title;
group.appendChild(fullTitle);
group.appendChild(svgElement('rect', {
class: node.styleKey,
x: node.x,
y: node.y,
width: node.width,
height: node.height,
rx: 16,
}));
const hasHint = Boolean(node.displayHint);
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));
group.addEventListener('keydown', event => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
openModal(node.nodeKey, group);
}
});
nodeLayer.appendChild(group);
});
}
function getNodeComponents(nodeKey) {
if (!currentData || !currentData.layout) return [];
const node = currentData.layout.nodes.find(item => item.nodeKey === nodeKey);
if (!node) return [];
const componentMap = new Map(currentData.components.map(component => [component.key, component]));
return node.memberKeys.map(key => componentMap.get(key)).filter(Boolean);
}
function getLayoutNode(nodeKey) {
return currentData?.layout?.nodes.find(node => node.nodeKey === nodeKey);
}
function buildConnectionsForComponent(componentKey) {
const result = [];
currentData.connections.forEach(connection => {
const fromComponent = splitEndpoint(connection.fromPort);
const toComponent = splitEndpoint(connection.toPort);
if (fromComponent !== componentKey && toComponent !== componentKey) return;
const labels = {
structural_mount: '结构直连',
pressurized_passage: '加压走道',
fuel: '燃料管线',
power: '供电管线',
docking: '对接',
cooling: '冷却回路',
};
const otherKey = fromComponent === componentKey ? toComponent : fromComponent;
const other = currentData.components.find(component => component.key === otherKey);
result.push(`${other?.name || otherKey}${labels[connection.type] || connection.type}`);
});
return result;
}
function openModal(nodeKey, trigger = null) {
const components = getNodeComponents(nodeKey);
if (components.length === 0) return;
const primary = components[0];
const layoutNode = getLayoutNode(nodeKey);
document.getElementById('detail-type').textContent =
components.length > 1 ? '组合模块' : (TYPE_LABELS[primary.type] || primary.type);
document.getElementById('detail-title').textContent =
layoutNode?.labelOverride || primary.name;
document.getElementById('detail-role').textContent =
components.map(component => component.description || component.summary || component.name).join('');
const ports = components.flatMap(component =>
(component.ports || []).map(port => `${component.name}${port.displayName}${port.direction || '—'}`)
);
document.getElementById('detail-location').textContent = ports.join(' · ') || '资料未记录';
const statusParts = [];
components.forEach(component => {
const states = component.states || {};
const values = [];
if (states.lifecycle) values.push(states.lifecycle === 'installed' ? '已安装' : states.lifecycle);
if (states.operation) {
values.push({ operational: '运行中', standby: '待机', offline: '离线' }[states.operation] || states.operation);
if (states.operationDetails?.powerPercent) values.push(`功率 ${states.operationDetails.powerPercent}%`);
}
if (values.length) statusParts.push(`${component.name}${values.join(' · ')}`);
});
document.getElementById('detail-status').textContent = statusParts.join('') || '资料未记录';
const metrics = [];
components.forEach(component => {
const dimensions = component.dimensions || {};
const properties = component.properties || {};
const values = [];
if (dimensions.massT) values.push(`质量 ${dimensions.massT} t`);
if (properties.capacity) values.push(`铺位 ${properties.capacity}`);
if (properties.sleepBags) values.push(`睡袋 ${properties.sleepBags}`);
if (properties.zones) values.push(`温区 ${properties.zones}`);
if (properties.foldingArms) values.push(`折叠臂 ${properties.foldingArms}`);
if (properties.block) values.push(`Block ${properties.block}`);
if (values.length) metrics.push(`${component.name}${values.join('')}`);
else if (components.length > 1 && component.summary) metrics.push(`${component.name}${component.summary}`);
});
setLines('detail-metrics', metrics, '资料未记录');
const dimensionLines = [];
components.forEach(component => {
const dimensions = component.dimensions || {};
const values = [];
if (dimensions.lengthM) values.push(`长 ${dimensions.lengthM} m`);
if (dimensions.widthM) values.push(`宽 ${dimensions.widthM} m`);
if (dimensions.heightM) values.push(`高 ${dimensions.heightM} m`);
if (dimensions.diameterM) values.push(`直径 ${dimensions.diameterM} m`);
if (values.length) dimensionLines.push(`${component.name}${values.join('')}`);
});
setLines('detail-dimensions', dimensionLines, '资料未记录');
const connections = [];
components.forEach(component => {
buildConnectionsForComponent(component.key).forEach(value => {
if (!connections.includes(value)) connections.push(value);
});
});
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();
}
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';
setError('');
document.getElementById('map-shell').style.display = 'none';
try {
currentData = await fetchSnapshot(mission, at);
renderTopology();
const query = currentData.query;
const missionLabel = query.requestedMission || '(最新)';
document.getElementById('page-subtitle').textContent =
`${missionLabel} · 快照时间: ${formatSnapshotTime(query.snapshotAt)} · ${currentData.components.length} 组件`;
document.getElementById('snapshot-info').textContent =
`快照: ${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';
setError(`加载失败: ${error.message}`);
}
}
(async () => {
const missionSelect = document.getElementById('mission-select');
const missions = await loadMissions();
missions.forEach(mission => {
const option = document.createElement('option');
option.value = mission.code;
option.textContent = mission.code;
missionSelect.appendChild(option);
});
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();
});
document.querySelector('.modal-close').addEventListener('click', closeModal);
document.addEventListener('keydown', event => {
if (event.key === 'Escape' && document.getElementById('detail-modal').getAttribute('aria-hidden') === 'false') {
closeModal();
}
});
})();
</script>
</body>
</html>