- Reference docs: known issues, user journey, location redesign, mission merge plan - Scripts: fill_location_state, merge_missions, migrate_location, migrate_state - Tests: e2e test suite, asset entries unit tests - Demo: location board, hifi prototypes, test screenshots - Data backup: asset log entries and state nodes - Update .gitignore Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
735 lines
66 KiB
HTML
735 lines
66 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN" data-theme="dark">
|
||
<head>
|
||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>KSP管理后台</title>
|
||
<style>
|
||
:root{--bg:#0B0B10;--sfc:#131318;--sfc2:#1a1a22;--brd:rgba(248,250,252,.06);--txt:#F8FAFC;--mut:#94A3B8;--acc:#3B82F6;--ag:rgba(59,130,246,.3);--grn:#22c55e;--amb:#eab308;--red:#ef4444;--cyn:#06b6d4;--pur:#a855f7;--rad:6px;--sw:224px;--th:42px;--fd:'SF Mono','Cascadia Code',Consolas,monospace;--fu:system-ui,-apple-system,'Segoe UI',sans-serif}
|
||
[data-theme="light"]{--bg:#F1F5F9;--sfc:#FFFFFF;--sfc2:#F8FAFC;--brd:rgba(0,0,0,.06);--txt:#0F172A;--mut:#64748B;--acc:#2563EB;--ag:rgba(37,99,235,.15)}
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
body{background:var(--bg);color:var(--txt);font-family:var(--fu);height:100vh;overflow:hidden;display:flex;flex-direction:column}
|
||
::-webkit-scrollbar{width:4px;height:4px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--brd);border-radius:2px}
|
||
|
||
/* Topbar */
|
||
#tb{height:var(--th);display:flex;align-items:center;gap:12px;padding:0 14px;background:var(--sfc);border-bottom:1px solid var(--brd);z-index:30;flex-shrink:0}
|
||
#tb .br{font-family:var(--fd);font-size:.78rem;font-weight:600;letter-spacing:-.02em}#tb .br b{color:var(--acc);font-weight:700}
|
||
#tb .tb-sp{flex:1}
|
||
.tb-time{display:flex;align-items:center;gap:5px}
|
||
.tb-time .l{font-size:.62rem;color:var(--mut);text-transform:uppercase;letter-spacing:.05em}
|
||
.tb-time #td{font-family:var(--fd);font-size:.74rem;color:var(--acc);background:rgba(59,130,246,.06);padding:3px 8px;border-radius:4px;cursor:pointer;border:1px solid transparent;transition:.15s}
|
||
.tb-time #td:hover{background:rgba(59,130,246,.12);border-color:var(--ag)}
|
||
.b{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:4px;font-size:.7rem;cursor:pointer;border:1px solid var(--brd);background:transparent;color:var(--mut);transition:.15s;white-space:nowrap}
|
||
.b:hover{background:rgba(128,128,128,.08);color:var(--txt)}.b.ac{background:var(--acc);color:#fff;border-color:var(--acc)}.b.ac:hover{box-shadow:0 0 10px var(--ag)}
|
||
.b.sm{padding:2px 6px;font-size:.64rem}.b.dg{border-color:var(--red);color:var(--red)}.b.dg:hover{background:rgba(239,68,68,.08)}
|
||
|
||
/* Layout */
|
||
#ly{display:flex;flex:1;overflow:hidden}
|
||
#sb{width:var(--sw);flex-shrink:0;background:var(--sfc);border-right:1px solid var(--brd);overflow-y:auto;padding:6px 0;user-select:none}
|
||
.nst{display:flex;align-items:center;gap:6px;padding:8px 14px 2px;font-family:var(--fd);font-size:.6rem;font-weight:600;color:var(--mut);text-transform:uppercase;letter-spacing:.08em;cursor:pointer}.nst .ar{font-size:.5rem;transition:transform .15s;opacity:.4}.nst.on .ar{transform:rotate(-90deg)}.nst.on+.niw{display:none}
|
||
.niw{padding-bottom:2px}
|
||
.ni{display:flex;align-items:center;gap:8px;padding:6px 14px;cursor:pointer;font-size:.76rem;color:var(--mut);transition:.1s;border-left:2px solid transparent}
|
||
.ni:hover{color:var(--txt);background:rgba(128,128,128,.03)}.ni.act{color:var(--txt);background:rgba(59,130,246,.05);border-left-color:var(--acc)}
|
||
.ni .ic{width:16px;height:16px;display:flex;align-items:center;justify-content:center;font-size:.8rem;flex-shrink:0;opacity:.6}.ni.su{padding-left:36px;font-size:.72rem}
|
||
|
||
#ct{flex:1;overflow-y:auto;padding:20px 24px;position:relative}
|
||
.pg{display:none}.pg.act{display:block}.pg.loc{display:none;position:absolute;inset:0;padding:0 !important;z-index:20;background:var(--bg)}.pg.loc.act{display:flex;flex-direction:column}
|
||
|
||
/* Page Header */
|
||
.ph{margin-bottom:18px}.ph .ey{font-family:var(--fd);font-size:.6rem;color:var(--mut);text-transform:uppercase;letter-spacing:.08em;margin-bottom:2px}.ph h1{font-family:var(--fd);font-size:1.1rem;font-weight:600}.ph p{color:var(--mut);font-size:.72rem;margin-top:1px}
|
||
.ph-row{display:flex;align-items:center;justify-content:space-between}
|
||
|
||
/* Metrics */
|
||
.mg{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:18px}
|
||
.mc{background:var(--sfc);border:1px solid var(--brd);border-radius:var(--rad);padding:14px 16px;transition:border-color .15s}.mc:hover{border-color:rgba(248,250,252,.1)}
|
||
.mc .l{font-family:var(--fd);font-size:.6rem;color:var(--mut);text-transform:uppercase;letter-spacing:.05em;margin-bottom:4px}.mc .v{font-family:var(--fd);font-size:1.6rem;font-weight:600}.mc .s{font-size:.66rem;color:var(--mut);margin-top:2px}
|
||
.mc.ac .v{color:var(--acc)}.mc.gn .v{color:var(--grn)}.mc.am .v{color:var(--amb)}
|
||
|
||
/* Status Board */
|
||
.stl{font-family:var(--fd);font-size:.68rem;font-weight:600;color:var(--mut);text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px}
|
||
.sgr{display:flex;flex-direction:column;gap:6px;margin-bottom:16px}
|
||
.sgi{background:var(--sfc);border:1px solid var(--brd);border-radius:var(--rad);overflow:hidden}
|
||
.sgh{display:flex;align-items:center;gap:8px;padding:8px 14px;border-bottom:1px solid var(--brd);font-family:var(--fd);font-size:.66rem;font-weight:500;cursor:pointer;user-select:none}
|
||
.sdt{width:6px;height:6px;border-radius:50%;flex-shrink:0}.sdt.tf{background:var(--amb);box-shadow:0 0 5px var(--amb)}.sdt.ex{background:var(--acc);box-shadow:0 0 5px var(--ag)}.sdt.mt{background:var(--pur);box-shadow:0 0 5px var(--pur)}.sdt.dk{background:var(--grn);box-shadow:0 0 5px var(--grn)}
|
||
.scc{color:var(--mut);font-size:.64rem;margin-left:auto}
|
||
.srw{display:flex;align-items:center;gap:10px;padding:5px 14px 5px 30px;font-size:.74rem;transition:background .1s}.srw:hover{background:rgba(128,128,128,.02)}
|
||
.srw .an{color:var(--acc);cursor:pointer;flex:1;font-weight:500}.srw .an:hover{text-decoration:underline}
|
||
.srw .am{color:var(--mut);font-size:.66rem}.srw .al{color:var(--mut);font-size:.66rem;font-family:var(--fd)}
|
||
|
||
/* Events */
|
||
.evl{display:flex;flex-direction:column;gap:3px;margin-bottom:18px}
|
||
.evr{display:flex;align-items:center;gap:10px;padding:7px 10px;background:var(--sfc);border:1px solid var(--brd);border-radius:var(--rad);font-size:.74rem}
|
||
.evr .ed{font-family:var(--fd);font-size:.66rem;color:var(--amb);min-width:76px}.evr .eb{font-family:var(--fd);font-size:.6rem;padding:2px 6px;border-radius:3px;background:rgba(234,179,8,.08);color:var(--amb)}
|
||
|
||
/* Filter Bar */
|
||
.fb{display:flex;align-items:center;gap:8px;margin-bottom:12px;flex-wrap:wrap}
|
||
.fb input[type=text],.fb select{padding:5px 8px;border-radius:4px;background:var(--sfc2);color:var(--txt);border:1px solid var(--brd);font-size:.72rem}
|
||
.fb input[type=text]{width:180px}.fb input[type=text]:focus,.fb select:focus{outline:none;border-color:var(--acc)}
|
||
.fb label{display:flex;align-items:center;gap:4px;font-size:.7rem;color:var(--mut);cursor:pointer}
|
||
|
||
/* Table */
|
||
table{width:100%;border-collapse:collapse;font-size:.74rem}
|
||
th,td{padding:6px 10px;text-align:left;border-bottom:1px solid var(--brd)}
|
||
th{font-family:var(--fd);font-size:.62rem;font-weight:600;color:var(--mut);text-transform:uppercase;letter-spacing:.04em;cursor:pointer;user-select:none;white-space:nowrap}
|
||
th:hover{color:var(--txt)}.th-sa{color:var(--acc);margin-left:2px}
|
||
td{color:var(--txt)}td a{color:var(--acc);text-decoration:none}td a:hover{text-decoration:underline}td.mt{color:var(--mut);font-size:.68rem}
|
||
tr:hover td{background:rgba(128,128,128,.015)}
|
||
.act-btn{display:inline-flex;gap:4px;font-size:.68rem}.act-btn a{color:var(--mut);text-decoration:none;padding:2px 5px;border-radius:3px;transition:.1s}.act-btn a:hover{background:var(--brd);color:var(--txt)}
|
||
|
||
/* Pagination */
|
||
.pg-bar{display:flex;align-items:center;gap:3px;margin-top:12px;font-size:.7rem}
|
||
.pg-bar span,.pg-bar a{padding:3px 7px;border-radius:4px;color:var(--mut);text-decoration:none}.pg-bar a:hover{background:var(--brd);color:var(--txt)}.pg-bar .cur{background:var(--acc);color:#fff}
|
||
|
||
/* Timeline */
|
||
.tl-r{display:flex;align-items:center;gap:8px;padding:5px 8px;margin-bottom:2px;background:var(--sfc);border:1px solid var(--brd);border-radius:var(--rad);font-size:.72rem}
|
||
.tl-bar{flex:1;height:5px;border-radius:3px;background:var(--brd);position:relative;overflow:hidden}
|
||
.tl-fl{position:absolute;top:0;height:100%;border-radius:3px;opacity:.75}.tl-fl.tf{background:var(--amb)}.tl-fl.ex{background:var(--acc)}.tl-fl.mt{background:var(--pur)}.tl-fl.dk{background:var(--grn)}
|
||
.tl-scale{display:flex;margin-bottom:6px;font-size:.58rem;color:var(--mut);text-align:center;border-bottom:1px solid var(--brd);padding-bottom:4px}
|
||
.tl-scale>div{flex:1}
|
||
|
||
/* Fuel Converter */
|
||
.cvw{display:flex;gap:16px}.cvw form{display:flex;flex-direction:column;gap:8px;width:260px}.cvw form label{font-size:.72rem;color:var(--mut)}.cvw form input,.cvw form select{padding:5px 8px;border-radius:4px;background:var(--sfc2);color:var(--txt);border:1px solid var(--brd)}.cvw .res{flex:1}
|
||
|
||
/* CRUD Modal */
|
||
.modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:100;display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .2s}.modal-bg.open{opacity:1;pointer-events:auto}
|
||
.modal{background:var(--sfc);border:1px solid var(--brd);border-radius:var(--rad);padding:20px;width:400px;max-height:80vh;overflow-y:auto}
|
||
.modal h2{font-family:var(--fd);font-size:.85rem;margin-bottom:14px}
|
||
.modal .fg{display:flex;flex-direction:column;gap:8px;margin-bottom:12px}
|
||
.modal .fg label{font-size:.72rem;color:var(--mut)}
|
||
.modal .fg input,.modal .fg select,.modal .fg textarea{padding:6px 8px;border-radius:4px;background:var(--bg);color:var(--txt);border:1px solid var(--brd);font-size:.74rem}
|
||
.modal .fg textarea{min-height:60px;resize:vertical}
|
||
.modal .fg input:focus,.modal .fg select:focus,.modal .fg textarea:focus{outline:none;border-color:var(--acc)}
|
||
.modal .btn-row{display:flex;gap:6px;justify-content:flex-end}
|
||
.modal .btn-row .b:first-child{margin-right:auto}
|
||
|
||
/* Location Board */
|
||
#map-ct{flex:1;position:relative;display:flex;align-items:center;justify-content:center;background:var(--bg);cursor:grab;overflow:hidden}
|
||
#map-cv{display:block}
|
||
#bk-btn{position:absolute;top:10px;left:10px;z-index:5;display:none;background:var(--sfc);border:1px solid var(--brd);color:var(--txt);padding:4px 12px;border-radius:var(--rad);cursor:pointer;font-size:.74rem}#bk-btn.vis{display:block}
|
||
#scope-ind{position:absolute;top:10px;right:10px;z-index:5;background:rgba(19,19,24,.85);border:1px solid var(--brd);color:var(--mut);padding:3px 10px;border-radius:4px;font-size:.68rem;font-family:var(--fd)}
|
||
#map-info{width:280px;background:var(--sfc);border-left:1px solid var(--brd);padding:16px;overflow-y:auto;display:flex;flex-direction:column;gap:14px;flex-shrink:0}
|
||
#map-info h2{font-family:var(--fd);font-size:.85rem;font-weight:600;padding-bottom:5px;border-bottom:1px solid var(--brd)}
|
||
.mi-sec h3{font-family:var(--fd);font-size:.6rem;text-transform:uppercase;color:var(--mut);letter-spacing:.05em;margin-bottom:3px}
|
||
.mi-val{font-family:var(--fd);font-size:1.3rem;font-weight:600;color:var(--acc)}
|
||
.mi-arow{display:flex;justify-content:space-between;align-items:center;padding:4px 0;border-bottom:1px solid var(--brd);font-size:.76rem}.mi-arow .n{color:var(--acc);cursor:pointer}.mi-arow .n:hover{text-decoration:underline}.mi-arow .loc{color:var(--mut);font-size:.68rem;font-family:var(--fd)}
|
||
|
||
#tl-layout{display:flex;flex:1;overflow:hidden}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="tb">
|
||
<div class="br"><b>KSP</b> 管理后台</div>
|
||
<div class="tb-sp"></div>
|
||
<div class="tb-time"><span class="l">Sim Time</span><span id="td" onclick="openTM()">2060-08-11 10:00</span></div>
|
||
<button class="b" onclick="shT(-1)">−1d</button><button class="b" onclick="shT(1)">+1d</button><button class="b" onclick="shT(7)">+7d</button><button class="b" onclick="rsT()">Today</button>
|
||
<button class="b" onclick="toggleTheme()" title="Toggle theme" style="margin-left:4px">◐</button>
|
||
</div>
|
||
|
||
<div id="ly">
|
||
<nav id="sb">
|
||
<a class="ni act" data-pg="overview" href="#"><span class="ic">◉</span>任务总览</a>
|
||
<div class="nst" onclick="this.classList.toggle('on')"><span class="ar">▾</span>任务运营</div>
|
||
<div class="niw">
|
||
<a class="ni su" data-pg="assets" href="#"><span class="ic">◆</span>任务资产</a>
|
||
<a class="ni su" data-pg="missions" href="#"><span class="ic">◈</span>任务列表</a>
|
||
<a class="ni su" data-pg="timeline" href="#"><span class="ic">◫</span>时间线</a>
|
||
<a class="ni su" data-pg="location" href="#"><span class="ic">◎</span>地点板</a>
|
||
</div>
|
||
<div class="nst" onclick="this.classList.toggle('on')"><span class="ar">▾</span>数据中心</div>
|
||
<div class="niw">
|
||
<a class="ni su" data-pg="engines" href="#"><span class="ic">⚙</span>引擎目录</a>
|
||
<a class="ni su" data-pg="comms" href="#"><span class="ic">◉</span>通信部件</a>
|
||
<a class="ni su" data-pg="tanks" href="#"><span class="ic">⬡</span>燃料箱规格</a>
|
||
<a class="ni su" data-pg="vehicles" href="#"><span class="ic">⇧</span>载具成本</a>
|
||
</div>
|
||
<a class="ni" data-pg="converter" href="#"><span class="ic">⇄</span>燃料换算</a>
|
||
</nav>
|
||
|
||
<main id="ct">
|
||
<!-- ═══ OVERVIEW ═══ -->
|
||
<div class="pg act" id="pg-overview">
|
||
<div class="ph"><div class="ey">Mission Operations</div><h1>任务总览</h1><p>Simulation: <strong id="ht"></strong> · All metrics derived from asset logs</p></div>
|
||
<div class="mg" id="metrics"></div>
|
||
<div class="stl">Status Board</div><div class="sgr" id="sgr"></div>
|
||
<div class="stl">Upcoming Events</div><div class="evl" id="evl"></div>
|
||
</div>
|
||
|
||
<!-- ═══ ASSETS ═══ -->
|
||
<div class="pg" id="pg-assets">
|
||
<div class="ph-row"><div class="ph"><div class="ey">Mission Operations</div><h1>任务资产</h1></div><div><button class="b ac" onclick="openCRUD('asset')">+ New Asset</button></div></div>
|
||
<div class="fb"><input type="text" id="aq" placeholder="Search..." oninput="rA()"><select id="at" onchange="rA()"><option value="">All Types</option><option>Exploration Mothership</option><option>Space Shuttle</option><option>Station</option><option>Base</option><option>Cargo Ship</option></select><select id="al" onchange="rA()"><option value="">All Locations</option><option>Earth System</option><option>Mars System</option><option>Jupiter System</option><option>Transfer</option></select><label><input type="checkbox" id="ah" onchange="rA()" checked> Hide retired</label></div>
|
||
<table id="atbl"><thead><tr>
|
||
<th onclick="sA('n')">Name <span class="th-sa"></span></th>
|
||
<th onclick="sA('t')">Type <span class="th-sa"></span></th>
|
||
<th onclick="sA('l')">Location <span class="th-sa"></span></th>
|
||
<th>Current Event</th><th onclick="sA('c')">Entries <span class="th-sa"></span></th><th>Action</th>
|
||
</tr></thead><tbody></tbody></table>
|
||
<div class="pg-bar" id="apg"></div>
|
||
</div>
|
||
|
||
<!-- ═══ ASSET DETAIL ═══ -->
|
||
<div class="pg" id="pg-asset-detail">
|
||
<div class="ph-row"><div class="ph"><div class="ey">Mission Asset</div><h1 id="ad-name">—</h1><p id="ad-sub"></p></div><div style="display:flex;gap:6px"><button class="b" onclick="openCRUD('asset',adIdx)">Edit Asset</button><button class="b" onclick="swPg('assets')">← Back</button></div></div>
|
||
<div style="display:grid;grid-template-columns:1fr 280px;gap:16px">
|
||
<div>
|
||
<!-- Info cards -->
|
||
<div class="mg" style="grid-template-columns:repeat(4,1fr);margin-bottom:12px">
|
||
<div class="mc"><div class="l">Type</div><div class="v" id="ad-type" style="font-size:.95rem">—</div></div>
|
||
<div class="mc"><div class="l">Location</div><div class="v" id="ad-loc" style="font-size:.95rem">—</div></div>
|
||
<div class="mc"><div class="l">Mission</div><div class="v" id="ad-miss" style="font-size:.85rem">—</div></div>
|
||
<div class="mc"><div class="l">Entries</div><div class="v" id="ad-entries" style="font-size:.95rem">0</div></div>
|
||
</div>
|
||
|
||
<!-- Docking section: target + vehicles -->
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px">
|
||
<div>
|
||
<div class="stl">Docking Target</div>
|
||
<div id="ad-target" style="background:var(--sfc);border:1px solid var(--brd);border-radius:var(--rad);padding:10px 12px;font-size:.78rem"></div>
|
||
</div>
|
||
<div>
|
||
<div class="stl" style="display:flex;align-items:center;justify-content:space-between">
|
||
<span>Docked Vehicles</span>
|
||
<button class="b sm ac" onclick="openDockModal()" style="font-size:.64rem">+ Dock</button>
|
||
</div>
|
||
<div id="ad-docked" style="background:var(--sfc);border:1px solid var(--brd);border-radius:var(--rad);padding:6px 0;font-size:.76rem;max-height:120px;overflow-y:auto"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Log entries -->
|
||
<div class="stl" style="display:flex;align-items:center;justify-content:space-between">
|
||
<span>Log Entries</span>
|
||
<div style="display:flex;gap:4px">
|
||
<button class="b sm ac" onclick="openNewLog('state')">+ State Interval</button>
|
||
<button class="b sm" onclick="openNewLog('event')">+ Event Point</button>
|
||
</div>
|
||
</div>
|
||
<div class="fb"><input type="text" id="ad-lq" placeholder="Search logs..." oninput="rADLogs()"><select id="ad-lf" onchange="rADLogs()"><option value="">All Types</option><option value="state">State Interval</option><option value="event">Event Point</option></select></div>
|
||
<table><thead><tr><th>Date</th><th>Type</th><th>Title</th><th>State</th><th>Location</th><th>Action</th></tr></thead><tbody id="ad-log-tbl"></tbody></table>
|
||
<div class="pg-bar" id="ad-log-pg"></div>
|
||
</div>
|
||
<div>
|
||
<!-- Upcoming events -->
|
||
<div class="stl">Upcoming Events</div>
|
||
<div class="evl" id="ad-events" style="margin-bottom:14px"></div>
|
||
<!-- Quick info -->
|
||
<div class="stl">Quick Info</div>
|
||
<div style="background:var(--sfc);border:1px solid var(--brd);border-radius:var(--rad);padding:10px 12px;font-size:.72rem;color:var(--mut)">
|
||
<div style="margin-bottom:4px"><strong style="color:var(--txt)">Retired:</strong> <span id="ad-retired">No</span></div>
|
||
<div style="margin-bottom:4px"><strong style="color:var(--txt)">Created:</strong> <span id="ad-created">—</span></div>
|
||
<div><strong style="color:var(--txt)">Updated:</strong> <span id="ad-updated">—</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ MISSIONS ═══ -->
|
||
<div class="pg" id="pg-missions">
|
||
<div class="ph"><div class="ey">Mission Operations</div><h1>任务列表</h1></div>
|
||
<div class="fb"><input type="text" id="mq" placeholder="Search..." oninput="rM()"><select id="ml" onchange="rM()"><option value="">All Locations</option><option>Earth System</option><option>Mars System</option><option>Jupiter System</option><option>Transfer</option></select></div>
|
||
<div class="sgr" id="msg"></div>
|
||
</div>
|
||
|
||
<!-- ═══ TIMELINE ═══ -->
|
||
<div class="pg" id="pg-timeline">
|
||
<div class="ph"><div class="ey">Mission Operations</div><h1>时间线</h1></div>
|
||
<!-- Filter bar row 1: search + presets + scale -->
|
||
<div class="fb"><input type="text" id="tl-q" placeholder="Search assets..." oninput="rT()" style="width:160px">
|
||
<div style="display:flex;gap:3px;margin-left:4px">
|
||
<button class="b sm tl-pre" data-f="active" onclick="tlPreset('active')">Active</button>
|
||
<button class="b sm tl-pre" data-f="month" onclick="tlPreset('month')">This Month</button>
|
||
<button class="b sm tl-pre" data-f="90d" onclick="tlPreset('90d')">Next 90d</button>
|
||
<button class="b sm tl-pre" data-f="all" onclick="tlPreset('all')">All</button>
|
||
</div>
|
||
<div style="margin-left:auto;display:flex;gap:4px;align-items:center">
|
||
<label style="font-size:.68rem;color:var(--mut)">Scale:</label>
|
||
<select id="tls" onchange="rT()" style="padding:3px 6px;border-radius:4px;background:var(--sfc2);color:var(--txt);border:1px solid var(--brd);font-size:.68rem"><option value="year">Year</option><option value="month" selected>Month</option><option value="day">Day</option></select>
|
||
</div>
|
||
</div>
|
||
<!-- Filter bar row 2: Type + Location + State + Date range -->
|
||
<div class="fb">
|
||
<label style="font-size:.68rem;color:var(--mut)">Type:</label><select id="tl-type" onchange="rT()" style="padding:3px 6px;border-radius:4px;background:var(--sfc2);color:var(--txt);border:1px solid var(--brd);font-size:.68rem"><option value="">All</option><option>Exploration Mothership</option><option>Space Shuttle</option><option>Station</option><option>Base</option><option>Cargo Ship</option></select>
|
||
<label style="font-size:.68rem;color:var(--mut)">Location:</label><select id="tl-loc" onchange="rT()" style="padding:3px 6px;border-radius:4px;background:var(--sfc2);color:var(--txt);border:1px solid var(--brd);font-size:.68rem"><option value="">All</option><option>Earth System</option><option>Mars System</option><option>Jupiter System</option><option>Transfer</option></select>
|
||
<label style="font-size:.68rem;color:var(--mut)">State:</label><select id="tl-state" onchange="rT()" style="padding:3px 6px;border-radius:4px;background:var(--sfc2);color:var(--txt);border:1px solid var(--brd);font-size:.68rem"><option value="">All</option><option value="transfer">Transfer</option><option value="exploration">Exploration</option><option value="maintenance">Maintenance</option><option value="docked">Docked</option></select>
|
||
<div style="margin-left:auto;display:flex;gap:4px;align-items:center">
|
||
<label style="font-size:.68rem;color:var(--mut)">From:</label><input type="date" id="tl-from" onchange="rT()" style="padding:3px 6px;border-radius:4px;background:var(--sfc2);color:var(--txt);border:1px solid var(--brd);font-size:.68rem">
|
||
<label style="font-size:.68rem;color:var(--mut)">To:</label><input type="date" id="tl-to" onchange="rT()" style="padding:3px 6px;border-radius:4px;background:var(--sfc2);color:var(--txt);border:1px solid var(--brd);font-size:.68rem">
|
||
</div>
|
||
</div>
|
||
<!-- Asset pill tags -->
|
||
<div class="fb" id="tl-pills" style="gap:4px"></div>
|
||
<div id="tlr"></div>
|
||
</div>
|
||
|
||
<!-- ═══ LOCATION BOARD ═══ -->
|
||
<div class="pg loc" id="pg-location">
|
||
<div id="tl-layout" style="flex:1;display:flex">
|
||
<div id="map-ct"><canvas id="map-cv"></canvas><button id="bk-btn" onclick="mapGoBack()">← Back</button><span id="scope-ind">Solar System</span></div>
|
||
<div id="map-info"><h2 id="mi-title">Solar System</h2>
|
||
<div class="mi-sec"><h3>Scope</h3><div id="mi-scope" style="font-size:.88rem;font-weight:500">—</div></div>
|
||
<div class="mi-sec"><h3>Assets in Scope</h3><div id="mi-assets"></div></div>
|
||
<div class="mi-sec"><h3>Ongoing Missions</h3><div class="mi-val" id="mi-miss">0</div></div>
|
||
<div class="mi-sec"><h3>Upcoming Events</h3><div id="mi-events"></div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ ENGINES ═══ -->
|
||
<div class="pg" id="pg-engines">
|
||
<div class="ph-row"><div class="ph"><div class="ey">Reference Data</div><h1>引擎目录</h1></div><div><button class="b ac" onclick="openCRUD('engine')">+ New Engine</button></div></div>
|
||
<div class="fb"><input type="text" id="eq" placeholder="Search..." oninput="rE()"><select id="ef" onchange="rE()"><option value="">All Fuel</option><option>LF/O</option><option>Nuclear</option><option>Electric</option></select></div>
|
||
<table><thead><tr><th onclick="sortT('eq-tbl',0)">Name</th><th onclick="sortT('eq-tbl',1)">Cycle</th><th onclick="sortT('eq-tbl',2)">Mass</th><th onclick="sortT('eq-tbl',3)">Vac Thrust</th><th onclick="sortT('eq-tbl',4)">Vac ISP</th><th>Fuel</th><th>Mod</th><th>Action</th></tr></thead><tbody id="eq-tbl"></tbody></table>
|
||
</div>
|
||
|
||
<!-- ═══ COMMS ═══ -->
|
||
<div class="pg" id="pg-comms">
|
||
<div class="ph-row"><div class="ph"><div class="ey">Reference Data</div><h1>通信部件</h1></div><div><button class="b ac" onclick="openCRUD('comm')">+ New</button></div></div>
|
||
<table><thead><tr><th onclick="sortT('cm-tbl',0)">Name</th><th onclick="sortT('cm-tbl',1)">Range</th><th onclick="sortT('cm-tbl',2)">Mass</th><th>Type</th><th>Source</th><th>Action</th></tr></thead><tbody id="cm-tbl"></tbody></table>
|
||
</div>
|
||
|
||
<!-- ═══ TANKS ═══ -->
|
||
<div class="pg" id="pg-tanks">
|
||
<div class="ph-row"><div class="ph"><div class="ey">Reference Data</div><h1>燃料箱规格</h1></div><div><button class="b ac" onclick="openCRUD('tank')">+ New</button></div></div>
|
||
<table><thead><tr><th onclick="sortT('tk-tbl',0)">Name</th><th onclick="sortT('tk-tbl',1)">Volume</th><th onclick="sortT('tk-tbl',2)">Dry Mass</th><th>Fuel</th><th>Source</th><th>Action</th></tr></thead><tbody id="tk-tbl"></tbody></table>
|
||
</div>
|
||
|
||
<!-- ═══ VEHICLES ═══ -->
|
||
<div class="pg" id="pg-vehicles">
|
||
<div class="ph-row"><div class="ph"><div class="ey">Reference Data</div><h1>载具成本</h1></div><div><button class="b ac" onclick="openCRUD('vehicle')">+ New</button></div></div>
|
||
<table><thead><tr><th onclick="sortT('vh-tbl',0)">Name</th><th onclick="sortT('vh-tbl',1)">Launch Cost</th><th>Source</th><th>Action</th></tr></thead><tbody id="vh-tbl"></tbody></table>
|
||
</div>
|
||
|
||
<!-- ═══ FUEL CONVERTER ═══ -->
|
||
<div class="pg" id="pg-converter">
|
||
<div class="ph"><div class="ey">Tools</div><h1>燃料换算</h1></div>
|
||
<div class="cvw"><form id="cvf"><label>Value<input type="number" id="cvv" value="1000" step="1"></label><label>Mode<select id="cvm"><option value="L2t">Liters → Tons</option><option value="t2L">Tons → Liters</option></select></label><label>Fuel<select id="cvf-sel"></select></label><button type="submit" class="b ac">Convert</button></form><div class="res"><table><thead><tr><th>Fuel</th><th>Density (kg/L)</th><th>Result</th></tr></thead><tbody id="cvr"></tbody></table></div></div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- ═══ MODALS ═══ -->
|
||
<div class="modal-bg" id="tmodal"><div class="modal"><h2>Set Simulation Time</h2><div class="fg"><label>Time<input type="datetime-local" id="tminp" step="60"></label></div><div class="btn-row"><button class="b" onclick="clTM()">Cancel</button><button class="b ac" onclick="apT()">Apply</button></div></div></div>
|
||
<div class="modal-bg" id="crud-modal"><div class="modal"><h2 id="crud-title">New Entry</h2><div class="fg" id="crud-fields"></div><div class="btn-row"><button class="b dg" id="crud-del" style="display:none" onclick="crudDel()">Delete</button><button class="b" onclick="clCRUD()">Cancel</button><button class="b ac" onclick="crudSave()">Save</button></div></div></div>
|
||
<div class="modal-bg" id="dock-modal"><div class="modal"><h2>Dock Vehicle</h2><div class="fg"><label>Vehicle<select id="dock-vehicle"><option value="">Select existing asset...</option><option>回声号</option><option>秃鹫 VS-01</option><option>擎天号</option><option value="_custom">Custom name...</option></select></label><input type="text" id="dock-custom" placeholder="Enter vehicle name..." style="display:none"><label>Note<textarea id="dock-note" rows="2" placeholder="Optional"></textarea></label></div><div class="btn-row"><button class="b" onclick="clDock()">Cancel</button><button class="b ac" onclick="saveDock()">Dock</button></div></div></div>
|
||
<div class="modal-bg" id="log-modal"><div class="modal" style="width:420px;max-height:80vh;overflow-y:auto"><h2 id="log-modal-title">New Log Entry</h2><div class="fg"><input type="hidden" id="log-type" value="state"><label>Title<input type="text" id="log-title" placeholder="Entry title"></label><div id="log-end-wrap"><label>Start At<input type="datetime-local" id="log-start" step="60"></label><label>End At<input type="datetime-local" id="log-end" step="60"></label></div><div id="log-state-wrap"><label>State<select id="log-state"><option value="">—</option><option>Transfer</option><option>Exploration</option><option>Maintenance</option><option>Docked</option><option>In Transit</option></select></label></div><label>Location<input type="text" id="log-location" placeholder="LEO / Mars Orbit / etc."></label><label>Summary<textarea id="log-summary" rows="2" placeholder="Optional"></textarea></label><div id="log-nodes-wrap"><hr style="border:none;border-top:1px solid var(--brd);margin:6px 0"><div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px"><span style="font-size:.7rem;color:var(--mut)">State Nodes (0-N)</span><button class="b sm" type="button" onclick="addNodeRow()" style="font-size:.62rem">+ Add Node</button></div><div id="nodes-list"></div></div></div><div class="btn-row"><button class="b" onclick="clLog()">Cancel</button><button class="b ac" onclick="saveLog()">Save</button></div></div></div>
|
||
|
||
<script>
|
||
// ═══ STATE ═══
|
||
const ST=new Date('2060-08-11T10:00:00Z');
|
||
let cPg='overview',asS={k:'n',d:1};
|
||
let mapVS=[{tp:'solar',ck:'sun'}],mapHv=null,mapDg=false,mapDS=null,mapDL=null,mapDD=false,mapVO={x:0,y:0},mapZm=1;
|
||
|
||
const ASSETS=[
|
||
{n:'ST-01 万星源号',t:'Exploration Mothership',l:'Transfer',s:'transfer',m:'OSSEM-1 → Neptune',e:'Arrival 2060-07-25',c:12,r:false},
|
||
{n:'ST-02 克里斯滕号',t:'Exploration Mothership',l:'Transfer',s:'transfer',m:'EE-2 → Earth',e:'Arrival 2060-08-26',c:14,r:false},
|
||
{n:'ST-03 Muelsyse',t:'Exploration Mothership',l:'LEO',s:'maintenance',m:'Post-Trial Maintenance',e:'Ends 2060-10-25',c:8,r:false},
|
||
{n:'XH-01 羲和号',t:'Exploration Mothership',l:'Jupiter System',s:'exploration',m:'Europa Expedition 3',e:'On station',c:22,r:false},
|
||
{n:'XH-02 太白号',t:'Exploration Mothership',l:'Mars Orbit',s:'docked',m:'Mars Expedition 4',e:'Surface ops',c:16,r:false},
|
||
{n:'XH-03 常曦号',t:'Exploration Mothership',l:'LEO',s:'maintenance',m:'Commissioning',e:'Ends 2060-06-10',c:5,r:false},
|
||
{n:'JW-01 金乌号',t:'Cargo Ship',l:'LEO',s:'docked',m:'Station Logistics',e:'Resupply',c:6,r:false},
|
||
{n:'星港空间站',t:'Station',l:'LEO',s:'docked',m:'Hub Operations',e:'XH-03 docked',c:18,r:false},
|
||
{n:'火星一号基地',t:'Base',l:'Mars Surface',s:'exploration',m:'Habitation',e:'XH-02 crew',c:8,r:false},
|
||
{n:'欧罗巴前哨站',t:'Base',l:'Europa Surface',s:'exploration',m:'Ice Drilling',e:'XH-01 crew',c:5,r:false},
|
||
{n:'回声号',t:'Space Shuttle',l:'LEO',s:'docked',m:'Station Ferry',e:'Next flight',c:3,r:false},
|
||
{n:'秃鹫 VS-01',t:'Space Shuttle',l:'Earth Surface',s:'maintenance',m:'Overhaul',e:'Retired',c:180,r:true},
|
||
{n:'擎天号',t:'Cargo Ship',l:'LEO',s:'docked',m:'Cargo Run',e:'Undock 2060-09',c:4,r:false},
|
||
{n:'凌霄空间站',t:'Station',l:'LEO',s:'maintenance',m:'Decommissioning',e:'Crew transfer',c:10,r:true},
|
||
];
|
||
const EVENTS=[{d:'2060-06-10',t:'XH-03 Maintenance Complete'},{d:'2060-06-15',t:'ST-03 Deep Space Test Begins'},{d:'2060-06-28',t:'ST-02 Returns to Earth'},{d:'2060-07-25',t:'ST-01 Arrives at Neptune'},{d:'2060-10-25',t:'ST-03 Commissioning Ends'},{d:'2060-09-01',t:'ST-02 Upgrade Begins'}];
|
||
const MISSIONS=[{n:'OSSEM-1',a:'ST-01',l:'Transfer',d:'Neptune',st:'2060-02-11',et:'—',s:'Active'},{n:'EE-2 Return',a:'ST-02',l:'Transfer',d:'Earth',st:'2060-06-28',et:'2060-08-26',s:'Active'},{n:'Post-Trial',a:'ST-03',l:'LEO',d:'LEO',st:'2060-03-07',et:'2060-10-25',s:'Active'},{n:'Europa Exp 3',a:'XH-01',l:'Jupiter',d:'Europa',st:'2060-02-01',et:'—',s:'Active'},{n:'Mars Exp 4',a:'XH-02',l:'Mars Orbit',d:'Mars Surface',st:'2059-06-01',et:'—',s:'Active'},{n:'Commissioning',a:'XH-03',l:'LEO',d:'LEO',st:'2059-12-09',et:'2060-06-10',s:'Active'},{n:'Station Ops',a:'星港',l:'LEO',d:'LEO',st:'2055-01-01',et:'—',s:'Active'},{n:'Habitation',a:'火星一号',l:'Mars Surface',d:'Mars',st:'2055-06-01',et:'—',s:'Active'}];
|
||
let ENGINES=[{n:'KS-25 Vector',cy:'Staged',m:4.0,vt:1000,vi:315,f:'LF/O',md:'Stock'},{n:'RE-L10 Poodle',cy:'Expander',m:1.75,vt:250,vi:350,f:'LF/O',md:'Stock'},{n:'LV-N Nerv',cy:'NTR',m:3.0,vt:60,vi:800,f:'Nuclear',md:'Stock'},{n:'IX-6315 Dawn',cy:'Ion',m:0.25,vt:2,vi:4200,f:'Electric',md:'Stock'},{n:'RE-M3 Mainsail',cy:'Staged',m:6.0,vt:1500,vi:310,f:'LF/O',md:'Stock'},{n:'CR-7 RAPIER',cy:'Combined',m:2.0,vt:180,vi:320,f:'LF/O',md:'Stock'}];
|
||
let COMMS=[{n:'Communotron 16',rng:'500M',mass:0.015,tp:'Direct',src:'Stock'},{n:'HG-5 High Gain',rng:'5M',mass:0.075,tp:'Relay',src:'Stock'},{n:'RA-2 Relay',rng:'2G',mass:0.15,tp:'Relay',src:'Stock'},{n:'RA-15 Relay',rng:'15G',mass:0.3,tp:'Relay',src:'Stock'}];
|
||
let TANKS=[{n:'Oscar-B',vol:40,mass:0.025,f:'LF/O',src:'Stock'},{n:'FL-T400',vol:400,mass:0.25,f:'LF/O',src:'Stock'},{n:'FL-T800',vol:800,mass:0.5,f:'LF/O',src:'Stock'},{n:'Jumbo-64',vol:6400,mass:4.0,f:'LF/O',src:'Stock'}];
|
||
let VEHICLES=[{n:'CERV Shuttle',cost:45000,src:'KSP'},{n:'Vulture B1',cost:38000,src:'KSP'},{n:'Echo Shuttle',cost:62000,src:'KSP'},{n:'Enterprise',cost:75000,src:'KSP'},{n:'Qingtian Cargo',cost:28000,src:'KSP'}];
|
||
const FUELS=[{n:'LF',d:.005},{n:'Oxidizer',d:.005},{n:'LF/O Mixed',d:.005},{n:'Monopropellant',d:.004},{n:'XenonGas',d:.0001},{n:'LH2',d:.00007085}];
|
||
|
||
const CRUD_DEFS={
|
||
asset:{title:'Asset',fields:[{k:'n',l:'Name'},{k:'t',l:'Type',type:'select',opts:['Exploration Mothership','Space Shuttle','Station','Base','Cargo Ship']},{k:'l',l:'Location'},{k:'s',l:'State',type:'select',opts:['transfer','exploration','maintenance','docked']},{k:'m',l:'Mission'},{k:'e',l:'Event'}],store:ASSETS},
|
||
engine:{title:'Engine',fields:[{k:'n',l:'Name'},{k:'cy',l:'Cycle'},{k:'m',l:'Mass (t)',type:'number'},{k:'vt',l:'Vac Thrust (kN)',type:'number'},{k:'vi',l:'Vac ISP (s)',type:'number'},{k:'f',l:'Fuel',type:'select',opts:['LF/O','Nuclear','Electric']},{k:'md',l:'Mod'}],store:ENGINES},
|
||
comm:{title:'Communication',fields:[{k:'n',l:'Name'},{k:'rng',l:'Range'},{k:'mass',l:'Mass (t)',type:'number'},{k:'tp',l:'Type',type:'select',opts:['Direct','Relay']},{k:'src',l:'Source'}],store:COMMS},
|
||
tank:{title:'Tank',fields:[{k:'n',l:'Name'},{k:'vol',l:'Volume (L)',type:'number'},{k:'mass',l:'Dry Mass (t)',type:'number'},{k:'f',l:'Fuel'},{k:'src',l:'Source'}],store:TANKS},
|
||
vehicle:{title:'Vehicle',fields:[{k:'n',l:'Name'},{k:'cost',l:'Launch Cost',type:'number'},{k:'src',l:'Source'}],store:VEHICLES},
|
||
};
|
||
|
||
// ═══ HELPERS ═══
|
||
function fm(d){return`${d.getUTCFullYear()}-${String(d.getUTCMonth()+1).padStart(2,'0')}-${String(d.getUTCDate()).padStart(2,'0')} ${String(d.getUTCHours()).padStart(2,'0')}:${String(d.getUTCMinutes()).padStart(2,'0')}`}
|
||
function uT(){const s=fm(ST);$('td').textContent=s;$('ht').textContent=s}
|
||
|
||
// ═══ NAV ═══
|
||
function $(s){return document.querySelector(s.startsWith('#')||s.startsWith('.')||s.startsWith('[')?s:'#'+s)}
|
||
function swPg(p){
|
||
document.querySelectorAll('.pg').forEach(e=>e.classList.remove('act'));
|
||
document.querySelectorAll('.ni').forEach(e=>e.classList.remove('act'));
|
||
const pg=$(`#pg-${p}`);if(pg)pg.classList.add('act');
|
||
const nav=$(`[data-pg="${p}"]`);if(nav)nav.classList.add('act');
|
||
cPg=p;
|
||
if(p==='overview'){rO();rEts()}else if(p==='assets')rA();else if(p==='asset-detail'&&adIdx>=0){}else if(p==='missions')rM();else if(p==='timeline'){rT();initTlPills()}else if(p==='engines')rE();else if(p==='comms')rCm();else if(p==='tanks')rTk();else if(p==='vehicles')rVh();else if(p==='converter')rCv();
|
||
if(p==='location'){initMap();setTimeout(()=>rMapInfo(),100)}
|
||
}
|
||
document.querySelectorAll('.ni').forEach(e=>{e.addEventListener('click',function(ev){ev.preventDefault();swPg(this.dataset.pg)})});
|
||
|
||
// ═══ OVERVIEW ═══
|
||
function rO(){
|
||
const act=ASSETS.filter(a=>!a.r).length,ms=MISSIONS.filter(m=>m.s==='Active').length,up=EVENTS.filter(e=>e.d>=ST.toISOString().slice(0,10)).length;
|
||
$('metrics').innerHTML=`<div class="mc ac"><div class="l">Tracked Assets</div><div class="v">${act}</div><div class="s">6 Motherships · 2 Stations · 2 Cargo · 2 Shuttles</div></div><div class="mc gn"><div class="l">Active Missions</div><div class="v">${ms}</div><div class="s">2 Deep Space · 1 Transfer · 2 Orbit · 1 Surface</div></div><div class="mc am"><div class="l">Upcoming (30d)</div><div class="v">${up}</div><div class="s">${up>0?EVENTS.filter(e=>e.d>=ST.toISOString().slice(0,10))[0].t:'None imminent'}</div></div>`;
|
||
const gr={transfer:{l:'In Transit',c:'tf',i:[]},exploration:{l:'Exploration',c:'ex',i:[]},maintenance:{l:'Maintenance',c:'mt',i:[]},docked:{l:'Docked / Surface',c:'dk',i:[]}};
|
||
for(const a of ASSETS){if(a.r)continue;gr[a.s].i.push(a)}
|
||
$('sgr').innerHTML=Object.values(gr).filter(g=>g.i.length).map(g=>`<div class="sgi"><div class="sgh"><span class="sdt ${g.c}"></span>${g.l}<span class="scc">${g.i.length}</span></div>${g.i.map(a=>`<div class="srw"><span class="an">${a.n}</span><span class="am">${a.m}</span><span class="al">${a.l}</span></div>`).join('')}</div>`).join('');
|
||
}
|
||
function rEts(){$('evl').innerHTML=EVENTS.filter(e=>e.d>=ST.toISOString().slice(0,10)).slice(0,6).map(e=>`<div class="evr"><span class="ed">${e.d}</span>${e.t}<span class="eb">${new Date(e.d+'T00:00Z')<new Date(ST.getTime()+30*86400000)?'< 30d':''}</span></div>`).join('')}
|
||
|
||
// ═══ ASSETS ═══
|
||
function rA(){
|
||
const q=($('aq').value||'').toLowerCase(),tp=$('at').value,loc=$('al').value,hd=$('ah').checked;
|
||
let rows=ASSETS.filter(a=>{if(hd&&a.r)return false;if(tp&&a.t!==tp)return false;if(loc&&!a.l.includes(loc))return false;if(q&&!a.n.toLowerCase().includes(q)&&!a.t.toLowerCase().includes(q))return false;return true});
|
||
rows.sort((a,b)=>{let va=a[asS.k]||'',vb=b[asS.k]||'';if(typeof va==='string'){va=va.toLowerCase();vb=vb.toLowerCase()}return va>vb?asS.d:va<vb?-asS.d:0});
|
||
const pp=10,tot=Math.ceil(rows.length/pp),pg=Math.min(Math.max(1,1),tot||1),pgd=rows.slice((pg-1)*pp,pg*pp);
|
||
$('atbl').querySelector('tbody').innerHTML=pgd.map(a=>`<tr><td><a href="#" onclick="openAssetDetail(${ASSETS.indexOf(a)});return false">${a.n}</a></td><td>${a.t}</td><td class="mt">${a.l}</td><td>${a.e||'—'}</td><td>${a.c}</td><td><div class="act-btn"><a href="#" onclick="openCRUD('asset',${ASSETS.indexOf(a)});return false">Edit</a><a href="#" onclick="delItem(ASSETS,${ASSETS.indexOf(a)});rA();return false">Del</a></div></td></tr>`).join('');
|
||
}
|
||
function sA(k){if(asS.k===k)asS.d*=-1;else{asS.k=k;asS.d=1}rA()}
|
||
|
||
// ═══ MISSIONS ═══
|
||
function rM(){
|
||
const q=($('mq').value||'').toLowerCase(),lc=$('ml').value;
|
||
let rows=MISSIONS.filter(m=>{if(lc&&!m.l.includes(lc)&&!m.d.includes(lc))return false;if(q&&!m.n.toLowerCase().includes(q)&&!m.a.toLowerCase().includes(q))return false;return true});
|
||
const g={};for(const m of rows){if(!g[m.l])g[m.l]=[];g[m.l].push(m)}
|
||
$('msg').innerHTML=Object.entries(g).map(([k,i])=>`<div class="sgi"><div class="sgh">${k}<span class="scc">${i.length}</span></div>${i.map(m=>`<div class="srw"><span class="an">${m.a}: ${m.n}</span><span class="am">${m.st} → ${m.d}</span><span class="al">${m.s}</span></div>`).join('')}</div>`).join('');
|
||
}
|
||
|
||
// ═══ TIMELINE ═══
|
||
let tlPresetVal='active',tlPills=[];
|
||
function tlPreset_set(f){tlPresetVal=f;document.querySelectorAll('.tl-pre').forEach(b=>b.classList.toggle('ac',b.dataset.f===f));rT()}
|
||
function tlPreset(f){tlPreset_set(f)}
|
||
function rT(){
|
||
const sc=$('tls').value,q=($('tl-q').value||'').toLowerCase(),tp=$('tl-type').value,loc=$('tl-loc').value,st=$('tl-state').value;
|
||
const from=$('tl-from').value,to=$('tl-to').value;
|
||
// Filter assets
|
||
let rows=ASSETS.filter(a=>{
|
||
if(tlPresetVal==='active'&&a.r)return false;
|
||
if(tp&&a.t!==tp)return false;
|
||
if(loc&&!a.l.includes(loc))return false;
|
||
if(st&&a.s!==st)return false;
|
||
if(q&&!a.n.toLowerCase().includes(q))return false;
|
||
if(tlPills.length&&!tlPills.some(p=>a.n.toLowerCase().includes(p.toLowerCase())))return false;
|
||
return true;
|
||
});
|
||
// Render scale
|
||
const ms=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
||
let h='<div class="tl-scale">';
|
||
for(let i=0;i<12;i++)h+=`<div>${ms[i]}</div>`;
|
||
h+='</div>';
|
||
for(const a of rows){
|
||
const w=Math.max(5,20+Math.random()*70);
|
||
h+=`<div class="tl-r"><span style="width:110px;font-size:.68rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="${a.n}">${a.n}</span><div class="tl-bar"><div class="tl-fl ${a.s}" style="left:${5+Math.random()*25}%;width:${w}%"></div></div></div>`;
|
||
}
|
||
$('tlr').innerHTML=h;
|
||
// Render pills
|
||
renderTlPills(rows);
|
||
}
|
||
function renderTlPills(rows){
|
||
const all=tlPresetVal==='active'?ASSETS.filter(a=>!a.r):ASSETS;
|
||
$('tl-pills').innerHTML=all.map(a=>{
|
||
const sel=tlPills.some(p=>p.toLowerCase()===a.n.toLowerCase());
|
||
return`<button class="b sm${sel?' ac':''}" onclick="tlTogglePill('${a.n}')" style="font-size:.64rem">${a.n}</button>`;
|
||
}).join('')+`<button class="b sm" onclick="tlPills=[];rT()" style="font-size:.64rem;color:var(--mut)">Clear</button>`;
|
||
}
|
||
function tlTogglePill(name){
|
||
const idx=tlPills.findIndex(p=>p.toLowerCase()===name.toLowerCase());
|
||
if(idx>=0)tlPills.splice(idx,1);else tlPills.push(name);
|
||
rT();
|
||
}
|
||
function initTlPills(){
|
||
$('tl-pills').innerHTML=ASSETS.filter(a=>!a.r).map(a=>
|
||
`<button class="b sm" onclick="tlTogglePill('${a.n}')" style="font-size:.64rem">${a.n}</button>`
|
||
).join('')+`<button class="b sm" onclick="tlPills=[];rT()" style="font-size:.64rem;color:var(--mut)">Clear</button>`;
|
||
}
|
||
|
||
// ═══ ASSET DETAIL ═══
|
||
let adIdx=-1;
|
||
const MOCK_LOGS=[
|
||
{t:'2060-02-11',k:'state',title:'Transfer to Neptune',state:'Transfer',loc:'Solar Orbit',nodes:[
|
||
{title:'Departed Earth orbit',at:'2060-02-11',detail:'Lightspeed engine burn'},
|
||
{title:'Mid-course correction',at:'2060-03-15',detail:'Delta-v 12 km/s'}
|
||
]},
|
||
{t:'2060-07-25',k:'state',title:'Neptune System Operations',state:'Exploration',loc:'Neptune Orbit',nodes:[]},
|
||
{t:'2059-11-25',k:'state',title:'Star Port Full Upgrade',state:'Maintenance',loc:'LEO',nodes:[
|
||
{title:'Central extension installed',at:'2059-12-10',detail:''},
|
||
{title:'Cryo-sleep chambers deployed',at:'2060-01-15',detail:'12 chambers'}
|
||
]},
|
||
{t:'2059-11-24',k:'event',title:'Arrived at Earth from Saturn',state:'—',loc:'LEO',nodes:[]},
|
||
{t:'2058-06-01',k:'state',title:'Saturn Exploration Mission 1',state:'Exploration',loc:'Saturn System',nodes:[]},
|
||
{t:'2058-01-05',k:'state',title:'Maintenance and Upgrades',state:'Maintenance',loc:'LEO',nodes:[]},
|
||
{t:'2057-12-20',k:'event',title:'Returned from Europa',state:'—',loc:'LEO',nodes:[]},
|
||
{t:'2056-12-15',k:'state',title:'Europa Outpost Construction 1',state:'Exploration',loc:'Jupiter System',nodes:[]},
|
||
{t:'2056-11-03',k:'event',title:'Returned from Mars',state:'—',loc:'LEO',nodes:[]},
|
||
{t:'2056-06-02',k:'state',title:'Mars One Construction Mission 2',state:'Exploration',loc:'Mars Orbit',nodes:[]},
|
||
];
|
||
// Docking: parent=asset idx this is docked to, vehicles=[{name,since,until}] (until=null=still docked)
|
||
const DOCK_DATA={
|
||
0:{parent:-1,vehicles:[]},1:{parent:-1,vehicles:[]},2:{parent:7,vehicles:[]},3:{parent:-1,vehicles:[]},
|
||
4:{parent:8,vehicles:[{name:'JW-01 金乌号',since:'2059-08-01',until:null}]},
|
||
5:{parent:7,vehicles:[]},6:{parent:7,vehicles:[]},
|
||
7:{parent:-1,vehicles:[{name:'XH-03 常曦号',since:'2060-03-07',until:null},{name:'JW-01 金乌号',since:'2060-06-01',until:null},{name:'回声号',since:'2060-01-15',until:null}]},
|
||
8:{parent:-1,vehicles:[{name:'XH-02 太白号',since:'2059-08-01',until:null}]},
|
||
9:{parent:-1,vehicles:[{name:'XH-01 羲和号',since:'2060-02-01',until:null}]},
|
||
10:{parent:7,vehicles:[]},11:{parent:-1,vehicles:[]},12:{parent:7,vehicles:[]},13:{parent:-1,vehicles:[]},
|
||
};
|
||
function openAssetDetail(idx){
|
||
adIdx=idx;const a=ASSETS[idx],dk=DOCK_DATA[idx]||{parent:-1,vehicles:[]};
|
||
$('ad-name').textContent=a.n;
|
||
$('ad-sub').textContent=`${a.t} · ${a.r?'Retired':'Active'}`;
|
||
$('ad-type').textContent=a.t;
|
||
$('ad-loc').textContent=a.l;
|
||
$('ad-miss').textContent=a.m;
|
||
$('ad-entries').textContent=MOCK_LOGS.length;
|
||
$('ad-retired').textContent=a.r?'Yes':'No';
|
||
$('ad-created').textContent='2050-03-15';
|
||
$('ad-updated').textContent='2060-02-10';
|
||
// Docking target (with undock)
|
||
if(dk.parent>=0){const p=ASSETS[dk.parent];
|
||
$('ad-target').innerHTML=`<div style="display:flex;align-items:center;justify-content:space-between;padding:6px 10px"><span><span style="color:var(--acc);cursor:pointer" onclick="openAssetDetail(${dk.parent})">${p.n}</span> <span class="mt">${p.l}</span></span><a href="#" onclick="undockTarget();return false" style="font-size:.68rem;color:var(--red)">分离</a></div>`;
|
||
}else{
|
||
$('ad-target').innerHTML='<div style="padding:6px 10px;color:var(--mut);font-size:.72rem">Not docked — <a href="#" onclick="dockToAsset();return false" style="color:var(--accent)">Dock to asset</a></div>';
|
||
}
|
||
// Docked vehicles (multiple, each with undock)
|
||
const active=dk.vehicles.filter(v=>!v.until);
|
||
$('ad-docked').innerHTML=active.length?active.map(v=>{
|
||
const ri=dk.vehicles.indexOf(v);
|
||
return`<div style="display:flex;align-items:center;justify-content:space-between;padding:5px 10px;border-bottom:1px solid var(--brd)"><span><strong style="color:var(--txt)">${v.name}</strong> <span class="mt">since ${v.since}</span></span><a href="#" onclick="undockVehicle(${ri});return false" style="font-size:.68rem;color:var(--red)">分离</a></div>`;
|
||
}).join(''):'<div style="padding:6px 10px;color:var(--mut);font-size:.72rem">No vehicles docked</div>';
|
||
// Events
|
||
const evts=EVENTS.filter(e=>e.d>=ST.toISOString().slice(0,10));
|
||
$('ad-events').innerHTML=evts.length?evts.map(e=>`<div class="evr"><span class="ed">${e.d}</span>${e.t}</div>`).join(''):'<div style="color:var(--mut);font-size:.72rem;padding:6px 0">No upcoming events</div>';
|
||
rADLogs();
|
||
swPg('asset-detail');
|
||
}
|
||
function undockTarget(){const dk=DOCK_DATA[adIdx];if(dk)dk.parent=-1;openAssetDetail(adIdx)}
|
||
function dockToAsset(){openDockModal()}
|
||
function undockVehicle(i){const dk=DOCK_DATA[adIdx];if(dk&&dk.vehicles[i]){dk.vehicles[i].until=ST.toISOString().slice(0,10);openAssetDetail(adIdx)}}
|
||
function rADLogs(){
|
||
const q=($('ad-lq')?.value||'').toLowerCase(),ft=$('ad-lf')?.value||'';
|
||
let logs=MOCK_LOGS.filter(l=>{
|
||
if(ft&&l.k!==ft)return false;
|
||
if(q&&!l.title.toLowerCase().includes(q))return false;
|
||
return true;
|
||
});
|
||
$('ad-log-tbl').innerHTML=logs.map(l=>{
|
||
const ri=MOCK_LOGS.indexOf(l);
|
||
const nodes=l.nodes&&l.nodes.length?l.nodes.map(n=>`<div class="mt" style="font-size:.62rem">→ ${n.title} (${n.at}${n.detail?' · '+n.detail:''})</div>`).join(''):'';
|
||
return`<tr><td class="mt">${l.t}${l.end?` → ${l.end}`:''}</td><td><span style="font-size:.64rem;padding:2px 5px;border-radius:3px;background:${l.k==='state'?'rgba(59,130,246,.1);color:var(--acc)':'rgba(234,179,8,.08);color:var(--amb)'}">${l.k==='state'?'State':'Event'}</span></td><td>${l.title}${nodes}</td><td class="mt">${l.state}</td><td class="mt">${l.loc}</td><td><div class="act-btn"><a href="#" onclick="openEditLog(${ri});return false" style="font-size:.66rem">Edit</a><a href="#" onclick="delLog(${ri});return false" style="font-size:.66rem;color:var(--red)">Del</a></div></td></tr>`;
|
||
}).join('');
|
||
}
|
||
|
||
// ═══ DATA CENTER ═══
|
||
function rE(){const q=($('eq').value||'').toLowerCase(),ft=$('ef').value;let r=ENGINES.filter(e=>{if(ft&&e.f!==ft)return false;if(q&&!e.n.toLowerCase().includes(q)&&!e.f.toLowerCase().includes(q))return false;return true});$('eq-tbl').innerHTML=r.map((e,i)=>`<tr><td><a href="#">${e.n}</a></td><td>${e.cy}</td><td>${e.m}t</td><td>${e.vt}kN</td><td>${e.vi}s</td><td>${e.f}</td><td>${e.md}</td><td><div class="act-btn"><a href="#" onclick="openCRUD('engine',${i});return false">Edit</a><a href="#" onclick="delItem(ENGINES,${i});rE();return false">Del</a></div></td></tr>`).join('')}
|
||
function rCm(){$('cm-tbl').innerHTML=COMMS.map((c,i)=>`<tr><td>${c.n}</td><td>${c.rng}</td><td>${c.mass}t</td><td>${c.tp}</td><td>${c.src}</td><td><div class="act-btn"><a href="#" onclick="openCRUD('comm',${i});return false">Edit</a><a href="#" onclick="delItem(COMMS,${i});rCm();return false">Del</a></div></td></tr>`).join('')}
|
||
function rTk(){$('tk-tbl').innerHTML=TANKS.map((t,i)=>`<tr><td>${t.n}</td><td>${t.vol}L</td><td>${t.mass}t</td><td>${t.f}</td><td>${t.src}</td><td><div class="act-btn"><a href="#" onclick="openCRUD('tank',${i});return false">Edit</a><a href="#" onclick="delItem(TANKS,${i});rTk();return false">Del</a></div></td></tr>`).join('')}
|
||
function rVh(){$('vh-tbl').innerHTML=VEHICLES.map((v,i)=>`<tr><td>${v.n}</td><td>${v.cost.toLocaleString()}</td><td>${v.src}</td><td><div class="act-btn"><a href="#" onclick="openCRUD('vehicle',${i});return false">Edit</a><a href="#" onclick="delItem(VEHICLES,${i});rVh();return false">Del</a></div></td></tr>`).join('')}
|
||
|
||
// ═══ FUEL CONVERTER ═══
|
||
function rCv(){$('cvf-sel').innerHTML=FUELS.map(f=>`<option>${f.n}</option>`).join('');$('cvr').innerHTML=FUELS.map(f=>`<tr><td>${f.n}</td><td>${f.d}</td><td>—</td></tr>`).join('')}
|
||
$('cvf').addEventListener('submit',function(e){e.preventDefault();const v=parseFloat($('cvv').value)||0,mode=$('cvm').value;$('cvr').innerHTML=FUELS.map(f=>{const r=mode==='L2t'?(v*f.d/1000).toFixed(4)+' t':(v*1000/f.d).toFixed(0)+' L';return`<tr><td>${f.n}</td><td>${f.d}</td><td style="font-family:var(--fd);color:var(--acc)">${r}</td></tr>`}).join('')});
|
||
|
||
// ═══ CRUD ═══
|
||
let crudType='',crudIdx=-1;
|
||
function openCRUD(type,idx){
|
||
crudType=type;crudIdx=idx!==undefined?idx:-1;
|
||
const def=CRUD_DEFS[type];$('crud-title').textContent=(idx!==undefined?'Edit ':'New ')+def.title;
|
||
$('crud-del').style.display=idx!==undefined?'inline-flex':'none';
|
||
$('crud-fields').innerHTML=def.fields.map(f=>{
|
||
const v=idx!==undefined?(def.store[idx][f.k]||''):'';
|
||
if(f.type==='select')return`<label>${f.l}<select data-k="${f.k}">${f.opts.map(o=>`<option${o===v?' selected':''}>${o}</option>`).join('')}</select></label>`;
|
||
if(f.type==='number')return`<label>${f.l}<input type="number" data-k="${f.k}" value="${v}" step="any"></label>`;
|
||
return`<label>${f.l}<input type="text" data-k="${f.k}" value="${v}"></label>`;
|
||
}).join('');
|
||
$('#crud-modal').classList.add('open');
|
||
}
|
||
function clCRUD(){$('#crud-modal').classList.remove('open')}
|
||
function crudSave(){
|
||
const def=CRUD_DEFS[crudType],obj={};
|
||
def.fields.forEach(f=>{const el=$(`#crud-modal [data-k="${f.k}"]`);obj[f.k]=f.type==='number'?parseFloat(el.value)||0:el.value});
|
||
if(crudIdx>=0)Object.assign(def.store[crudIdx],obj);else def.store.push(obj);
|
||
clCRUD();
|
||
if(cPg==='assets')rA();else if(cPg==='engines')rE();else if(cPg==='comms')rCm();else if(cPg==='tanks')rTk();else if(cPg==='vehicles')rVh();
|
||
}
|
||
function crudDel(){const def=CRUD_DEFS[crudType];if(crudIdx>=0){def.store.splice(crudIdx,1);clCRUD();if(cPg==='assets')rA();else if(cPg==='engines')rE();else if(cPg==='comms')rCm();else if(cPg==='tanks')rTk();else if(cPg==='vehicles')rVh()}}
|
||
// === DOCKING ===
|
||
function openDockModal(){$('dock-custom').style.display='none';$('dock-vehicle').value='';$('dock-note').value='';$('dock-modal').classList.add('open')}
|
||
function clDock(){$('dock-modal').classList.remove('open')}
|
||
function saveDock(){
|
||
const v=$('dock-vehicle').value;if(!v)return;
|
||
const name=v==='_custom'?$('dock-custom').value:v;if(!name)return;
|
||
const dk=DOCK_DATA[adIdx]||{parent:-1,vehicles:[]};DOCK_DATA[adIdx]=dk;
|
||
dk.vehicles.push({name,since:ST.toISOString().slice(0,10),until:null});
|
||
clDock();openAssetDetail(adIdx);
|
||
}
|
||
$('dock-vehicle').addEventListener('change',function(){$('dock-custom').style.display=this.value==='_custom'?'block':'none'});
|
||
|
||
// === LOG MODAL (multi-node) ===
|
||
let logNodeData=[];
|
||
function addNodeRow(title='',at='',detail=''){
|
||
logNodeData.push({title,at,detail});
|
||
renderNodeRows();
|
||
}
|
||
function removeNodeRow(i){logNodeData.splice(i,1);renderNodeRows()}
|
||
function renderNodeRows(){
|
||
$('nodes-list').innerHTML=logNodeData.length?logNodeData.map((n,i)=>`
|
||
<div style="background:var(--sfc2);border:1px solid var(--brd);border-radius:4px;padding:6px 8px;margin-bottom:4px">
|
||
<div style="display:flex;gap:6px;align-items:center">
|
||
<input type="text" value="${n.title}" placeholder="Node title" oninput="logNodeData[${i}].title=this.value" style="flex:1;padding:3px 6px;border-radius:3px;background:var(--bg);color:var(--txt);border:1px solid var(--brd);font-size:.7rem">
|
||
<input type="datetime-local" value="${n.at}" oninput="logNodeData[${i}].at=this.value" step="60" style="width:140px;padding:3px 6px;border-radius:3px;background:var(--bg);color:var(--txt);border:1px solid var(--brd);font-size:.7rem">
|
||
<button class="b sm" type="button" onclick="removeNodeRow(${i})" style="color:var(--red);font-size:.6rem">✕</button>
|
||
</div>
|
||
<input type="text" value="${n.detail}" placeholder="Detail (optional)" oninput="logNodeData[${i}].detail=this.value" style="width:100%;padding:3px 6px;border-radius:3px;background:var(--bg);color:var(--txt);border:1px solid var(--brd);font-size:.7rem;margin-top:3px">
|
||
</div>
|
||
`).join(''):'<div style="font-size:.68rem;color:var(--mut);padding:4px 0">No nodes. Click "+ Add Node" to add.</div>';
|
||
}
|
||
function openNewLog(type){
|
||
logNodeData=[];
|
||
$('log-type').value=type;$('log-title').value='';$('log-start').value='';$('log-end').value='';
|
||
$('log-state').value='';$('log-location').value='';$('log-summary').value='';
|
||
$('log-end-wrap').style.display=type==='state'?'block':'none';
|
||
$('log-state-wrap').style.display=type==='state'?'block':'none';
|
||
$('log-nodes-wrap').style.display=type==='state'?'block':'none';
|
||
renderNodeRows();$('log-modal-title').textContent='New Log Entry';
|
||
$('log-modal').classList.add('open');
|
||
}
|
||
function openEditLog(i){
|
||
editLogIdx=i;const l=MOCK_LOGS[i];
|
||
$('log-type').value=l.k;$('log-title').value=l.title;$('log-start').value=l.t;$('log-end').value=l.end||'';
|
||
$('log-state').value=l.state==='—'?'':l.state;$('log-location').value=l.loc==='—'?'':l.loc;$('log-summary').value=l.summary||'';
|
||
logNodeData=(l.nodes||[]).map(n=>({title:n.title,at:n.at,detail:n.detail||''}));
|
||
$('log-end-wrap').style.display=l.k==='state'?'block':'none';
|
||
$('log-state-wrap').style.display=l.k==='state'?'block':'none';
|
||
$('log-nodes-wrap').style.display=l.k==='state'?'block':'none';
|
||
renderNodeRows();$('log-modal-title').textContent='Edit Log Entry';
|
||
$('log-modal').classList.add('open');
|
||
}
|
||
function clLog(){$('log-modal').classList.remove('open')}
|
||
function saveLog(){
|
||
const type=$('log-type').value,title=$('log-title').value;if(!title)return;
|
||
const entry={t:($('log-start').value||'').slice(0,10)||ST.toISOString().slice(0,10),end:$('log-end').value?$('log-end').value.slice(0,10):null,k:type,title,state:$('log-state').value||'—',loc:$('log-location').value||'—',summary:$('log-summary').value,nodes:logNodeData.filter(n=>n.title).map(n=>({title:n.title,at:n.at||entry?.t||'',detail:n.detail}))};
|
||
if(editLogIdx>=0)MOCK_LOGS[editLogIdx]=entry;else MOCK_LOGS.unshift(entry);
|
||
editLogIdx=-1;rADLogs();clLog();$('ad-entries').textContent=MOCK_LOGS.length;
|
||
}
|
||
function delLog(i){if(confirm('Delete?')){MOCK_LOGS.splice(i,1);rADLogs();$('ad-entries').textContent=MOCK_LOGS.length}}
|
||
function delItem(arr,i){if(confirm('Delete this item?'))arr.splice(i,1)}
|
||
function sortT(id,col){const tb=document.getElementById(id).querySelector('tbody');const rows=Array.from(tb.rows);rows.sort((a,b)=>a.cells[col].textContent.localeCompare(b.cells[col].textContent));tb.append(...rows)}
|
||
|
||
// ═══ LOCATION BOARD ═══
|
||
const LOC_ASSETS=ASSETS;
|
||
const LOC_EVENTS=EVENTS;
|
||
const BODIES={sun:{n:'Sun',t:'star',r:20,fr:52,c:'#FDB813',c2:'#ff6b00'},mercury:{n:'Mercury',t:'p',r:5.5,fr:38,c:'#b5b5b5',c2:'#8a8a8a'},venus:{n:'Venus',t:'p',r:8,fr:44,c:'#e6c229',c2:'#c4952a'},earth:{n:'Earth',t:'p',r:9,fr:46,c:'#4da6ff',c2:'#1a56db',mo:{moon:{n:'Moon',r:2.8,c:'#ccc',c2:'#999',orb:40,P:27.3}}},mars:{n:'Mars',t:'p',r:7,fr:40,c:'#c1440e',c2:'#7a1f0a',mo:{phobos:{n:'Phobos',r:1.6,c:'#998',c2:'#665',orb:26,P:.32},deimos:{n:'Deimos',r:1.4,c:'#887',c2:'#554',orb:35,P:1.26}}},ceres:{n:'Ceres',t:'d',r:4,fr:32,c:'#8b8b8b',c2:'#6b6b6b'},vesta:{n:'Vesta',t:'d',r:3,fr:28,c:'#a0a0a0',c2:'#808080'},jupiter:{n:'Jupiter',t:'p',r:16,fr:60,c:'#c9b07c',c2:'#8b6914',mo:{io:{n:'Io',r:2.8,c:'#e8d44d',c2:'#b8a420',orb:50,P:1.77},europa:{n:'Europa',r:2.5,c:'#d4c9a8',c2:'#a09060',orb:62,P:3.55},ganymede:{n:'Ganymede',r:3.2,c:'#aaa',c2:'#777',orb:76,P:7.15},callisto:{n:'Callisto',r:2.8,c:'#666',c2:'#444',orb:90,P:16.7}}},saturn:{n:'Saturn',t:'p',r:13.5,fr:54,c:'#e0c97f',c2:'#b8952a',mo:{mimas:{n:'Mimas',r:1.8,c:'#bbb',c2:'#999',orb:44,P:.94},enceladus:{n:'Enceladus',r:2,c:'#dde',c2:'#bbc',orb:56,P:1.37},titan:{n:'Titan',r:3.5,c:'#d4a85c',c2:'#a07830',orb:90,P:15.95},iapetus:{n:'Iapetus',r:2.7,c:'#999',c2:'#666',orb:112,P:79.3}}},uranus:{n:'Uranus',t:'p',r:11,fr:46,c:'#7ec8e3',c2:'#4a90b0',mo:{miranda:{n:'Miranda',r:1.6,c:'#ccc',c2:'#aaa',orb:36,P:1.41},titania:{n:'Titania',r:3,c:'#bbb',c2:'#999',orb:64,P:8.71}}},neptune:{n:'Neptune',t:'p',r:10,fr:44,c:'#3f54ba',c2:'#1a2a80',mo:{triton:{n:'Triton',r:2.8,c:'#b8d4e3',c2:'#80a0b0',orb:44,P:5.88}}},pluto:{n:'Pluto',t:'d',r:3,fr:28,c:'#d4c9a8',c2:'#a09070',mo:{charon:{n:'Charon',r:2.2,c:'#bbb',c2:'#999',orb:28,P:6.39}}}};
|
||
const SO=['mercury','venus','earth','mars','ceres','vesta','jupiter','saturn','uranus','neptune','pluto'];
|
||
for(const bk of Object.keys(BODIES)){const b=BODIES[bk];if(b.mo)for(const[mk,mb]of Object.entries(b.mo))BODIES[mk]=mb}
|
||
const J2000_JD=2451545.0,DAY_MS=86400000;
|
||
const ORB={mercury:{a:.387098,e:.205630,i:7.005,O:48.331,p:77.456,L:252.251,P:87.969},venus:{a:.723332,e:.006772,i:3.395,O:76.680,p:131.563,L:181.980,P:224.701},earth:{a:1.000003,e:.016709,i:.000,O:.000,p:102.938,L:100.464,P:365.256},mars:{a:1.523679,e:.093401,i:1.850,O:49.558,p:336.060,L:355.453,P:686.980},ceres:{a:2.767500,e:.075823,i:10.593,O:80.306,p:72.716,L:73.598,P:1680},vesta:{a:2.361000,e:.088740,i:7.141,O:103.852,p:150.047,L:18.900,P:1325},jupiter:{a:5.203363,e:.048393,i:1.305,O:100.556,p:14.754,L:34.404,P:4332.589},saturn:{a:9.537070,e:.054151,i:2.485,O:113.715,p:92.432,L:49.944,P:10759.22},uranus:{a:19.19126,e:.047168,i:.770,O:74.230,p:170.964,L:313.232,P:30685.4},neptune:{a:30.06904,e:.008606,i:1.770,O:131.723,p:44.971,L:304.880,P:60189},pluto:{a:39.48212,e:.248828,i:17.140,O:110.304,p:224.069,L:238.929,P:90560}};
|
||
function jd(d){return d.getTime()/DAY_MS+2440587.5}
|
||
function kepl(M,e,eps=1e-8){let E=M+e*Math.sin(M)*(1+e*Math.cos(M));for(let i=0;i<20;i++){let d=(M-E+e*Math.sin(E))/(1-e*Math.cos(E));E+=d;if(Math.abs(d)<eps)break}return E}
|
||
function helio(k,jv){const o=ORB[k];if(!o)return{x:0,y:0};const D=Math.PI/180,n=2*Math.PI/o.P,ds=jv-J2000_JD;let M=(o.L-o.p)*D+n*ds;M=((M%(2*Math.PI))+2*Math.PI)%(2*Math.PI);const E=kepl(M,o.e),s=Math.sin(E),c=Math.cos(E),r=o.a*(1-o.e*c),sv=Math.sqrt(1-o.e*o.e)*s/(1-o.e*c),cv=(c-o.e)/(1-o.e*c);const w=(o.p-o.O)*D,O=o.O*D,I=o.i*D,cO=Math.cos(O),sO=Math.sin(O),cI=Math.cos(I),sI=Math.sin(I);const xo=r*cv,yo=r*sv,cw=Math.cos(w),sw=Math.sin(w);const xe=xo*(cO*cw-sO*sw*cI)-yo*(cO*sw+sO*cw*cI),ye=xo*(sO*cw+cO*sw*cI)+yo*(cO*cw*cI-sO*sw);const dist=Math.sqrt(xe*xe+ye*ye),dd=Math.pow(dist,.4)*100,sc=dist>.001?dd/dist:0;return{x:xe*sc,y:ye*sc}}
|
||
let mapCtx,mapCv,mapStars=null;
|
||
function initMap(){
|
||
if(mapCv)return;
|
||
mapCv=document.getElementById('map-cv');mapCtx=mapCv.getContext('2d');
|
||
function rsz(){const r=document.getElementById('map-ct').getBoundingClientRect(),d=devicePixelRatio||1;if(r.width===0||r.height===0)return;mapCv.width=r.width*d;mapCv.height=r.height*d;mapCv.style.width=r.width+'px';mapCv.style.height=r.height+'px';mapStars=null;renderMap()}
|
||
window.addEventListener('resize',rsz);
|
||
setTimeout(rsz,100);
|
||
// Events
|
||
mapCv.addEventListener('mousedown',e=>{mapDg=true;mapDS={x:e.clientX,y:e.clientY};mapDL={x:e.clientX,y:e.clientY};mapDD=false});
|
||
window.addEventListener('mousemove',e=>{if(!mapDg||!mapDL)return;if(Math.abs(e.clientX-mapDS.x)>6||Math.abs(e.clientY-mapDS.y)>6)mapDD=true;if(mapDD){mapVO.x+=e.clientX-mapDL.x;mapVO.y+=e.clientY-mapDL.y;mapDL={x:e.clientX,y:e.clientY};renderMap()}});
|
||
window.addEventListener('mouseup',()=>{mapDg=false;mapDS=null;mapDL=null});
|
||
mapCv.addEventListener('wheel',e=>{e.preventDefault();mapZm=Math.max(.3,Math.min(4,mapZm*(e.deltaY>0?.9:1.1)));renderMap()},{passive:false});
|
||
mapCv.addEventListener('click',e=>{if(mapDD)return;const r=mapCv.getBoundingClientRect(),t=mapHit(e.clientX-r.left,e.clientY-r.top);if(!t)return;const v=mapVS[mapVS.length-1],b=BODIES[t];if(v.tp==='solar')mapVS.push({tp:b&&b.mo?'planet_system':'single',ck:t});else if(v.tp==='planet_system')mapVS.push({tp:'single',ck:t});mapVO={x:0,y:0};mapZm=1;updateMapBack();renderMap();rMapInfo()});
|
||
mapCv.addEventListener('mousemove',e=>{if(mapDD)return;const r=mapCv.getBoundingClientRect(),t=mapHit(e.clientX-r.left,e.clientY-r.top);if(t!==mapHv){mapHv=t;mapCv.style.cursor=t?'pointer':'grab';renderMap()}});
|
||
}
|
||
function mapFV(k){if(BODIES[k])return BODIES[k];for(const bk of Object.keys(BODIES)){const b=BODIES[bk];if(b.mo&&b.mo[k])return b.mo[k]}return null}
|
||
function mapHit(mx,my){
|
||
const v=mapVS[mapVS.length-1],cs=[];
|
||
if(v.tp==='solar'){cs.push({k:'sun'});for(const k of SO)cs.push({k})}
|
||
else if(v.tp==='planet_system'){const c=BODIES[v.ck];if(c){cs.push({k:v.ck});if(c.mo)for(const mk of Object.keys(c.mo))cs.push({k:mk})}}
|
||
else{const b=BODIES[v.ck];if(b)cs.push({k:v.ck})}
|
||
for(const{k}of cs){const b=mapFV(k);if(!b||b._x==null)continue;const dx=mx-b._x,dy=my-b._y,hr=(b._r||4)+6;if(dx*dx+dy*dy<=hr*hr)return k}return null
|
||
}
|
||
function mapDraw(ox,oy,rad,key,lb){
|
||
const b=BODIES[key]||mapFV(key);if(!b)return;
|
||
const isStar=b.t==='star',bc=b.c||'#888';
|
||
mapCtx.save();mapCtx.beginPath();mapCtx.arc(ox,oy,rad,0,Math.PI*2);mapCtx.clip();
|
||
// Gradient sphere
|
||
const g=mapCtx.createRadialGradient(ox-rad*.25,oy-rad*.3,0,ox,oy,rad);
|
||
if(isStar){g.addColorStop(0,'#fffff8');g.addColorStop(.3,'#ffcc44');g.addColorStop(.7,'#ff6600');g.addColorStop(1,'#330800')}
|
||
else{g.addColorStop(0,'#ffffff');g.addColorStop(.2,bc);g.addColorStop(.8,b.c2||'#333');g.addColorStop(1,'#111')}
|
||
mapCtx.fillStyle=g;mapCtx.beginPath();mapCtx.arc(ox,oy,rad,0,Math.PI*2);mapCtx.fill();mapCtx.restore();
|
||
// Shade
|
||
if(!isStar){mapCtx.save();mapCtx.beginPath();mapCtx.arc(ox,oy,rad,0,Math.PI*2);mapCtx.clip();
|
||
const sh=mapCtx.createRadialGradient(ox-rad*.3,oy-rad*.3,rad*.05,ox+rad*.15,oy+rad*.15,rad*1.1);sh.addColorStop(0,'rgba(255,255,255,.1)');sh.addColorStop(.4,'rgba(0,0,0,0)');sh.addColorStop(.8,'rgba(0,0,0,.5)');mapCtx.fillStyle=sh;mapCtx.fillRect(ox-rad,oy-rad,rad*2,rad*2);mapCtx.restore()}
|
||
mapCtx.fillStyle='#e2e8f0';mapCtx.font=`${Math.max(8,rad*.45)}px system-ui`;mapCtx.textAlign='center';mapCtx.fillText(lb,ox,oy-rad-4);
|
||
b._x=ox;b._y=oy;b._r=rad;
|
||
}
|
||
function renderMap(){
|
||
if(!mapCv||mapCv.width===0||mapCv.height===0)return;
|
||
const dpr=devicePixelRatio||1,W=mapCv.width/dpr,H=mapCv.height/dpr;
|
||
mapCtx.save();mapCtx.setTransform(dpr,0,0,dpr,0,0);
|
||
if(!mapStars||mapStars.width!==mapCv.width){
|
||
const sc=document.createElement('canvas');sc.width=W;sc.height=H;
|
||
const sx=sc.getContext('2d');sx.fillStyle=document.documentElement.getAttribute('data-theme')==='light'?'#1a1a2e':'#0B0B10';sx.fillRect(0,0,W,H);
|
||
for(let i=0;i<600;i++){sx.fillStyle=`rgba(255,255,255,${.2+Math.random()*.6})`;sx.beginPath();sx.arc(Math.random()*W,Math.random()*H,Math.random()*1.2+.1,0,Math.PI*2);sx.fill()}
|
||
mapStars=sc;
|
||
}
|
||
mapCtx.drawImage(mapStars,0,0,W,H);
|
||
const v=mapVS[mapVS.length-1],cx=W/2+mapVO.x,cy=H/2+mapVO.y,jv=jd(new Date(ST.getTime()));
|
||
if(v.tp==='solar'){
|
||
for(const k of SO){mapCtx.beginPath();mapCtx.arc(cx,cy,Math.pow(ORB[k].a,.4)*100*mapZm,0,Math.PI*2);mapCtx.strokeStyle='rgba(148,163,184,.1)';mapCtx.lineWidth=.5;mapCtx.setLineDash([3,6]);mapCtx.stroke();mapCtx.setLineDash([])}
|
||
mapDraw(cx,cy,BODIES.sun.r*mapZm,'sun','Sun');
|
||
for(const k of SO){const b=BODIES[k],p=helio(k,jv);mapDraw(cx+p.x*mapZm,cy-p.y*mapZm,b.r*mapZm,k,b.n)}
|
||
}else if(v.tp==='planet_system'){
|
||
const c=BODIES[v.ck];if(!c)return;
|
||
mapDraw(cx,cy,c.fr*mapZm,v.ck,c.n);
|
||
if(c.mo)for(const[mk,mb]of Object.entries(c.mo)){const od=mb.orb*mapZm*1.8;mapCtx.beginPath();mapCtx.arc(cx,cy,od,0,Math.PI*2);mapCtx.strokeStyle='rgba(148,163,184,.1)';mapCtx.lineWidth=.5;mapCtx.setLineDash([3,6]);mapCtx.stroke();mapCtx.setLineDash([]);const an=(2*Math.PI*((jv-J2000_JD)%mb.P))/mb.P;mapDraw(cx+od*Math.cos(an),cy-od*Math.sin(an),mb.r*2.2*mapZm,mk,mb.n)}
|
||
}else{
|
||
const b=BODIES[v.ck];if(!b)return;const mx=Math.min(cx,cy)*.5,rr=Math.min((b.fr?b.fr*3:(b.r||4)*15)*mapZm,mx);
|
||
mapDraw(cx,cy,rr,v.ck,b.n);
|
||
// Saturn rings
|
||
if(v.ck==='saturn'&&rr>6){mapCtx.save();mapCtx.strokeStyle='rgba(200,180,140,.3)';mapCtx.lineWidth=Math.max(1,rr*.06);mapCtx.beginPath();mapCtx.ellipse(cx,cy,rr*1.5,rr*.18,0,0,Math.PI*2);mapCtx.stroke();mapCtx.strokeStyle='rgba(200,180,140,.15)';mapCtx.lineWidth=Math.max(1,rr*.04);mapCtx.beginPath();mapCtx.ellipse(cx,cy,rr*1.8,rr*.28,0,0,Math.PI*2);mapCtx.stroke();mapCtx.restore()}
|
||
}
|
||
// Hover
|
||
if(mapHv){const b=mapFV(mapHv);if(b&&b._x!=null){mapCtx.beginPath();mapCtx.arc(b._x,b._y,(b._r||4)+5,0,Math.PI*2);mapCtx.strokeStyle='rgba(59,130,246,.7)';mapCtx.lineWidth=2;mapCtx.stroke()}}
|
||
mapCtx.restore();
|
||
}
|
||
function rMapInfo(){
|
||
const v=mapVS[mapVS.length-1];let sk,sn;
|
||
if(v.tp==='solar'){sk='solar_system';sn='Solar System'}else{sk=v.ck;const b=BODIES[v.ck];sn=b?b.n+(v.tp==='planet_system'?' System':''):v.ck}
|
||
$('mi-title').textContent=sn;$('mi-scope').textContent=sn;
|
||
const as=LOC_ASSETS.filter(a=>{
|
||
if(sk==='solar_system')return true;
|
||
const loc=a.l.toLowerCase();
|
||
if(sk==='earth')return loc.includes('leo')||loc.includes('earth');
|
||
if(sk==='mars')return loc.includes('mars');
|
||
if(sk==='jupiter')return loc.includes('jupiter')||loc.includes('europa');
|
||
if(sk==='saturn')return false;if(sk==='uranus')return false;if(sk==='neptune')return false;
|
||
return loc.includes(sk);
|
||
});
|
||
$('mi-assets').innerHTML=as.length?as.map(a=>`<div class="mi-arow"><span class="n">${a.n}</span><span class="loc">${a.l}</span></div>`).join(''):'<div style="color:var(--mut);font-size:.72rem">No assets</div>';
|
||
$('mi-miss').textContent=as.filter(a=>a.s==='transfer'||a.s==='exploration').length;
|
||
const ns=ST.toISOString().slice(0,10);
|
||
$('mi-events').innerHTML=LOC_EVENTS.filter(e=>e.d>=ns).slice(0,4).map(e=>`<div style="padding:2px 0;font-size:.7rem;border-bottom:1px solid var(--brd)"><span style="color:var(--amb);font-family:var(--fd);font-size:.64rem">${e.d}</span> ${e.t}</div>`).join('')||'<div style="color:var(--mut);font-size:.72rem">None</div>';
|
||
}
|
||
function updateMapBack(){$('bk-btn').classList.toggle('vis',mapVS.length>1)}
|
||
function mapGoBack(){if(mapVS.length>1)mapVS.pop();mapVO={x:0,y:0};mapZm=1;updateMapBack();renderMap();rMapInfo()}
|
||
|
||
// ═══ THEME ═══
|
||
function toggleTheme(){const cur=document.documentElement.getAttribute('data-theme'),next=cur==='dark'?'light':'dark';document.documentElement.setAttribute('data-theme',next);localStorage.setItem('ksp-theme',next);mapStars=null;if(mapCv)renderMap()}
|
||
(function(){const t=localStorage.getItem('ksp-theme');if(t)document.documentElement.setAttribute('data-theme',t)})();
|
||
|
||
// ═══ TIME ═══
|
||
function openTM(){$('tminp').value=`${ST.getUTCFullYear()}-${String(ST.getUTCMonth()+1).padStart(2,'0')}-${String(ST.getUTCDate()).padStart(2,'0')}T${String(ST.getUTCHours()).padStart(2,'0')}:${String(ST.getUTCMinutes()).padStart(2,'0')}`;$('tmodal').classList.add('open')}
|
||
function clTM(){$('tmodal').classList.remove('open')}
|
||
function apT(){const v=$('tminp').value;if(!v)return;const d=new Date(v+'Z');if(isNaN(d.getTime()))return;ST.setTime(d.getTime());uT();rO();rEts();clTM()}
|
||
function shT(d){ST.setTime(ST.getTime()+d*86400000);uT();rO();rEts()}
|
||
function rsT(){ST.setTime(new Date('2060-08-11T10:00:00Z').getTime());uT();rO();rEts()}
|
||
$('tmodal').addEventListener('click',function(e){if(e.target===this)clTM()});
|
||
document.addEventListener('keydown',e=>{if(e.key==='Escape'){clTM();clCRUD()}if(e.key==='t'&&e.ctrlKey){e.preventDefault();openTM()}});
|
||
|
||
// ═══ INIT ═══
|
||
uT();rO();rEts();rCv();
|
||
</script>
|
||
</body>
|
||
</html>
|