chore: add reference docs, scripts, tests, demo, prototypes
- 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>
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 59 KiB |
@@ -0,0 +1,734 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,420 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>KSP管理后台 — Location Board</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg:#0B0B10;--surface:#131318;--border:rgba(248,250,252,.08);--text:#F8FAFC;--muted:#94A3B8;
|
||||
--accent:#3B82F6;--accent-glow:rgba(59,130,246,.35);--warn:#eab308;--danger:#EF4444;
|
||||
--radius:8px;--transition:150ms ease;
|
||||
}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{
|
||||
background:var(--bg);color:var(--text);font-family:system-ui,-apple-system,'Segoe UI',sans-serif;
|
||||
height:100vh;overflow:hidden;display:flex;flex-direction:column;
|
||||
}
|
||||
/* Top bar */
|
||||
#top-bar{
|
||||
display:flex;align-items:center;gap:12px;padding:8px 16px;
|
||||
background:var(--surface);border-bottom:1px solid var(--border);z-index:20;
|
||||
font-size:.8rem;
|
||||
}
|
||||
#top-bar .brand{font-family:'SF Mono','Cascadia Code','Consolas',monospace;font-weight:600;font-size:.84rem;color:var(--text);letter-spacing:-.02em}
|
||||
#top-bar .brand span{color:var(--accent)}
|
||||
#scope-label{font-size:.78rem;color:var(--accent);font-weight:500}
|
||||
.spacer{flex:1}
|
||||
#time-controls{display:flex;align-items:center;gap:4px}
|
||||
#time-controls button,.style-btn{
|
||||
background:rgba(255,255,255,.05);border:1px solid transparent;color:var(--muted);
|
||||
padding:4px 9px;border-radius:4px;cursor:pointer;font-family:'SF Mono','Cascadia Code','Consolas',monospace;font-size:.72rem;
|
||||
transition:all var(--transition);
|
||||
}
|
||||
#time-controls button:hover{background:rgba(255,255,255,.1);color:var(--text);border-color:var(--border)}
|
||||
.style-btn.active{background:var(--accent);color:#fff;border-color:var(--accent)}
|
||||
#sim-time-input{
|
||||
font-family:'SF Mono','Cascadia Code','Consolas',monospace;font-size:.78rem;padding:4px 8px;border-radius:4px;
|
||||
background:rgba(255,255,255,.04);color:var(--text);border:1px solid var(--border);width:152px;text-align:center;
|
||||
}
|
||||
#sim-time-input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-glow)}
|
||||
#time-slider{
|
||||
flex:1;min-width:100px;max-width:240px;-webkit-appearance:none;height:3px;
|
||||
background:var(--border);border-radius:2px;outline:none;cursor:pointer;
|
||||
}
|
||||
#time-slider::-webkit-slider-thumb{-webkit-appearance:none;width:13px;height:13px;border-radius:50%;background:var(--accent);cursor:pointer;box-shadow:0 0 10px var(--accent-glow)}
|
||||
/* Main layout */
|
||||
#main{display:flex;flex:1;overflow:hidden}
|
||||
/* Map */
|
||||
#map-container{
|
||||
flex:1;position:relative;display:flex;align-items:center;justify-content:center;
|
||||
background:var(--bg);cursor:grab;overflow:hidden;
|
||||
}
|
||||
canvas{display:block}
|
||||
/* Map overlays */
|
||||
#back-btn{
|
||||
position:absolute;top:12px;left:12px;z-index:5;display:none;
|
||||
background:var(--surface);border:1px solid var(--border);color:var(--text);
|
||||
padding:5px 14px;border-radius:var(--radius);cursor:pointer;
|
||||
font-family:'Fira Sans',sans-serif;font-size:.78rem;
|
||||
transition:all var(--transition);
|
||||
}
|
||||
#back-btn.visible{display:block}
|
||||
#back-btn:hover{background:rgba(255,255,255,.08);border-color:rgba(248,250,252,.15)}
|
||||
#scope-indicator{
|
||||
position:absolute;top:12px;right:12px;z-index:5;
|
||||
background:rgba(19,19,24,.85);border:1px solid var(--border);
|
||||
color:var(--muted);padding:4px 12px;border-radius:4px;font-size:.72rem;
|
||||
font-family:'SF Mono','Cascadia Code','Consolas',monospace;
|
||||
}
|
||||
#style-toggle{position:absolute;bottom:12px;left:12px;z-index:5;display:flex;gap:4px}
|
||||
#instructions{
|
||||
position:absolute;bottom:12px;right:12px;z-index:5;
|
||||
color:rgba(148,163,184,.4);font-size:.66rem;font-family:'Fira Sans',sans-serif;
|
||||
pointer-events:none;
|
||||
}
|
||||
/* Info panel */
|
||||
#info-panel{
|
||||
width:320px;background:var(--surface);border-left:1px solid var(--border);
|
||||
padding:20px 18px;overflow-y:auto;display:flex;flex-direction:column;gap:18px;
|
||||
}
|
||||
#info-panel h2{
|
||||
font-family:'SF Mono','Cascadia Code','Consolas',monospace;font-size:.88rem;font-weight:600;
|
||||
padding-bottom:6px;border-bottom:1px solid var(--border);letter-spacing:-.02em;
|
||||
}
|
||||
.info-section{}
|
||||
.info-section h3{
|
||||
font-family:'SF Mono','Cascadia Code','Consolas',monospace;font-size:.66rem;text-transform:uppercase;
|
||||
color:var(--muted);letter-spacing:.06em;margin-bottom:4px;font-weight:500;
|
||||
}
|
||||
.metric-value{font-family:'SF Mono','Cascadia Code','Consolas',monospace;font-size:1.5rem;font-weight:600;color:var(--accent)}
|
||||
.asset-row{
|
||||
display:flex;justify-content:space-between;align-items:center;
|
||||
padding:6px 0;border-bottom:1px solid rgba(248,250,252,.04);font-size:.8rem;
|
||||
}
|
||||
.asset-row .name{color:var(--accent);cursor:pointer;transition:color var(--transition)}
|
||||
.asset-row .name:hover{color:#60a5fa;text-decoration:underline}
|
||||
.asset-row .loc{color:var(--muted);font-size:.72rem;font-family:'SF Mono','Cascadia Code','Consolas',monospace}
|
||||
.event-row{
|
||||
padding:4px 0;font-size:.78rem;border-bottom:1px solid rgba(248,250,252,.04);
|
||||
}
|
||||
.event-row .date{color:var(--warn);font-family:'SF Mono','Cascadia Code','Consolas',monospace;font-size:.72rem}
|
||||
/* Scrollbar */
|
||||
#info-panel::-webkit-scrollbar{width:4px}
|
||||
#info-panel::-webkit-scrollbar-track{background:transparent}
|
||||
#info-panel::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="top-bar">
|
||||
<div class="brand"><span>KSP</span>管理后台</div>
|
||||
<span style="color:var(--muted);font-size:.7rem">/</span>
|
||||
<span id="scope-label">Solar System</span>
|
||||
<span class="spacer"></span>
|
||||
<div id="time-controls">
|
||||
<button onclick="shiftDays(-365)" title="-1 year">◀◀</button>
|
||||
<button onclick="shiftDays(-30)" title="-30 days">◀</button>
|
||||
<input type="text" id="sim-time-input" value="2060-08-11 10:00" title="YYYY-MM-DD HH:MM">
|
||||
<button onclick="shiftDays(30)" title="+30 days">▶</button>
|
||||
<button onclick="shiftDays(365)" title="+1 year">▶▶</button>
|
||||
<button onclick="resetTime()">Today</button>
|
||||
</div>
|
||||
<input type="range" id="time-slider" min="0" max="1000" value="0">
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
<div id="map-container">
|
||||
<button id="back-btn" onclick="goBack()">← Back</button>
|
||||
<span id="scope-indicator">Solar System</span>
|
||||
<div id="style-toggle">
|
||||
<button class="style-btn" id="btn-flat" onclick="setStyle('flat')">Flat</button>
|
||||
<button class="style-btn active" id="btn-realistic" onclick="setStyle('realistic')">Realistic</button>
|
||||
</div>
|
||||
<span id="instructions">click planet to focus · drag to pan · scroll to zoom</span>
|
||||
<canvas id="map-canvas"></canvas>
|
||||
</div>
|
||||
<div id="info-panel">
|
||||
<h2 id="panel-title">Solar System</h2>
|
||||
<div class="info-section"><h3>Scope</h3><div style="font-size:.92rem;font-weight:500" id="panel-scope">—</div></div>
|
||||
<div class="info-section"><h3>Assets in Scope</h3><div id="panel-assets"></div></div>
|
||||
<div class="info-section"><h3>Ongoing Missions</h3><div class="metric-value" id="panel-mission-count">—</div></div>
|
||||
<div class="info-section"><h3>Upcoming Events</h3><div id="panel-events"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ═══════════════════════════════════
|
||||
// KEPLERIAN ORBITAL ELEMENTS (J2000)
|
||||
// ═══════════════════════════════════
|
||||
const J2000_JD=2451545.0,DAY=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+2440587.5}
|
||||
function kepler(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(key,jv){
|
||||
const o=ORB[key];if(!o)return{x:0,y:0,r: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=kepler(M,o.e),s=Math.sin(E),c=Math.cos(E),r=o.a*(1-o.e*c);
|
||||
const 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);
|
||||
const ye=xo*(sO*cw+cO*sw*cI)+yo*(cO*cw*cI-sO*sw);
|
||||
const dist=Math.sqrt(xe*xe+ye*ye),ddisp=Math.pow(dist,.4)*100,scl=dist>.001?ddisp/dist:0;
|
||||
return{x:xe*scl,y:ye*scl,r:ddisp}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════
|
||||
// BODIES
|
||||
// ═══════════════════════════════════
|
||||
const BD={
|
||||
sun:{n:'Sun',t:'star',r:20,fr:52,c:'#FDB813',c2:'#ff6b00',moons:null},
|
||||
mercury:{n:'Mercury',t:'planet',r:5.5,fr:38,c:'#b5b5b5',c2:'#8a8a8a',moons:null},
|
||||
venus:{n:'Venus',t:'planet',r:8,fr:44,c:'#e6c229',c2:'#c4952a',moons:null},
|
||||
earth:{n:'Earth',t:'planet',r:9,fr:46,c:'#4da6ff',c2:'#1a56db',
|
||||
moons:{moon:{n:'Moon',r:2.8,c:'#ccc',c2:'#999',orb:40,P:27.3}}},
|
||||
mars:{n:'Mars',t:'planet',r:7,fr:40,c:'#c1440e',c2:'#7a1f0a',
|
||||
moons:{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:'dwarf',r:4,fr:32,c:'#8b8b8b',c2:'#6b6b6b',moons:null},
|
||||
vesta:{n:'Vesta',t:'ast',r:3,fr:28,c:'#a0a0a0',c2:'#808080',moons:null},
|
||||
jupiter:{n:'Jupiter',t:'planet',r:16,fr:60,c:'#c9b07c',c2:'#8b6914',
|
||||
moons:{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:'planet',r:13.5,fr:54,c:'#e0c97f',c2:'#b8952a',
|
||||
moons:{mimas:{n:'Mimas',r:1.8,c:'#bbb',c2:'#999',orb:44,P:.94},
|
||||
enceladus:{n:'Enceladus',r:2,c:'#dde',c2:'#bbc',orb:52,P:1.37},
|
||||
tethys:{n:'Tethys',r:2.2,c:'#ddd',c2:'#bbb',orb:60,P:1.89},
|
||||
dione:{n:'Dione',r:2.3,c:'#ccc',c2:'#aaa',orb:68,P:2.74},
|
||||
rhea:{n:'Rhea',r:2.5,c:'#ddd',c2:'#bbb',orb:78,P:4.52},
|
||||
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:106,P:79.3}}},
|
||||
uranus:{n:'Uranus',t:'planet',r:11,fr:46,c:'#7ec8e3',c2:'#4a90b0',
|
||||
moons:{miranda:{n:'Miranda',r:1.6,c:'#ccc',c2:'#aaa',orb:36,P:1.41},
|
||||
ariel:{n:'Ariel',r:2.2,c:'#ddd',c2:'#bbb',orb:46,P:2.52},
|
||||
umbriel:{n:'Umbriel',r:2.1,c:'#999',c2:'#777',orb:54,P:4.14},
|
||||
titania:{n:'Titania',r:2.8,c:'#bbb',c2:'#999',orb:64,P:8.71},
|
||||
oberon:{n:'Oberon',r:2.5,c:'#aaa',c2:'#888',orb:74,P:13.46}}},
|
||||
neptune:{n:'Neptune',t:'planet',r:10,fr:44,c:'#3f54ba',c2:'#1a2a80',
|
||||
moons:{triton:{n:'Triton',r:2.8,c:'#b8d4e3',c2:'#80a0b0',orb:44,P:5.88}}},
|
||||
pluto:{n:'Pluto',t:'dwarf',r:3,fr:28,c:'#d4c9a8',c2:'#a09070',
|
||||
moons:{charon:{n:'Charon',r:2.2,c:'#bbb',c2:'#999',orb:28,P:6.39}}}
|
||||
};
|
||||
// Flatten moons into BD for direct lookup
|
||||
for(const bk of Object.keys(BD)){const b=BD[bk];if(b.moons)for(const[mk,mb]of Object.entries(b.moons))BD[mk]=mb}
|
||||
const SO=['mercury','venus','earth','mars','ceres','vesta','jupiter','saturn','uranus','neptune','pluto'];
|
||||
const MOCK_ASSETS=[
|
||||
{id:'st01',n:'ST-01 万星源号',loc:'Transfer',sc:['solar_system']},
|
||||
{id:'st02',n:'ST-02 克里斯滕号',loc:'Jupiter System',sc:['solar_system','jupiter']},
|
||||
{id:'st03',n:'ST-03 Muelsyse',loc:'LEO',sc:['solar_system','earth']},
|
||||
{id:'xh01',n:'XH-01 羲和号',loc:'Jupiter System',sc:['solar_system','jupiter']},
|
||||
{id:'xh02',n:'XH-02 太白号',loc:'Mars Orbit',sc:['solar_system','mars']},
|
||||
{id:'xh03',n:'XH-03 常曦号',loc:'LEO',sc:['solar_system','earth']},
|
||||
{id:'jw01',n:'JW-01 金乌号',loc:'LEO',sc:['solar_system','earth']},
|
||||
{id:'sp',n:'星港空间站',loc:'LEO',sc:['solar_system','earth']},
|
||||
{id:'m1',n:'火星一号基地',loc:'Mars Surface',sc:['solar_system','mars']},
|
||||
{id:'eo',n:'欧罗巴前哨站',loc:'Europa Surface',sc:['solar_system','jupiter']},
|
||||
];
|
||||
const MOCK_EVENTS=[
|
||||
{d:'2060-06-10',t:'ST-03 维护结束'},{d:'2060-06-15',t:'ST-03 深空测试开始'},
|
||||
{d:'2060-06-28',t:'ST-02 返航抵达地球'},{d:'2060-07-25',t:'ST-01 抵达海王星系统'},
|
||||
{d:'2060-09-01',t:'ST-02 进入升级'},{d:'2060-10-25',t:'ST-03 服役前维护结束'},
|
||||
];
|
||||
|
||||
// ═══════════════════════════════════
|
||||
// STARFIELD & REAL TEXTURES
|
||||
// ═══════════════════════════════════
|
||||
let stars=null;
|
||||
function mkStars(w,h){
|
||||
const c=document.createElement('canvas');c.width=w;c.height=h;const x=c.getContext('2d');
|
||||
x.fillStyle='#0B0B10';x.fillRect(0,0,w,h);
|
||||
for(let i=0;i<800;i++){const px=Math.random()*w,py=Math.random()*h,s=Math.random()*1.6+.15;x.fillStyle=`rgba(255,255,255,${.3+Math.random()*.7})`;x.beginPath();x.arc(px,py,s,0,Math.PI*2);x.fill()}
|
||||
return c
|
||||
}
|
||||
|
||||
// Real texture images loaded from textures/ folder
|
||||
const TEX_SIZE=512;
|
||||
const TEX_MAP={
|
||||
sun:'sun.jpg',mercury:'mercury.jpg',venus:'venus.jpg',earth:'earth.jpg',
|
||||
mars:'mars.jpg',jupiter:'jupiter.jpg',saturn:'saturn.jpg',uranus:'uranus.jpg',
|
||||
neptune:'neptune.jpg',pluto:'pluto.jpg',moon:'moon.jpg',io:'io.jpg'
|
||||
};
|
||||
const texImgs={};
|
||||
let texLoaded=0;
|
||||
const texTotal=Object.keys(TEX_MAP).length;
|
||||
|
||||
function loadTextures(){
|
||||
for(const[key,file]of Object.entries(TEX_MAP)){
|
||||
const img=new Image();
|
||||
img.onload=()=>{texImgs[key]=img;texLoaded++;if(texLoaded>=texTotal)render()};
|
||||
img.src='textures/'+file;
|
||||
}
|
||||
}
|
||||
|
||||
function tex(key){
|
||||
if(texImgs[key])return texImgs[key];
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3D sphere shading overlay (drawn on top of texture)
|
||||
function shadeSphere(ctx,x,y,r,isStar){
|
||||
if(isStar){
|
||||
// Sun: subtle glow
|
||||
const glow=ctx.createRadialGradient(x,y,r*.6,x,y,r*1.4);
|
||||
glow.addColorStop(0,'rgba(255,200,50,.15)');glow.addColorStop(.5,'rgba(255,150,20,.05)');glow.addColorStop(1,'rgba(255,100,0,0)');
|
||||
ctx.fillStyle=glow;ctx.beginPath();ctx.arc(x,y,r*1.4,0,Math.PI*2);ctx.fill();
|
||||
return;
|
||||
}
|
||||
// 3D sphere shadow
|
||||
ctx.save();ctx.beginPath();ctx.arc(x,y,r,0,Math.PI*2);ctx.clip();
|
||||
const shadow=ctx.createRadialGradient(x-r*.3,y-r*.3,r*.05,x+r*.15,y+r*.15,r*1.1);
|
||||
shadow.addColorStop(0,'rgba(255,255,255,.1)');shadow.addColorStop(.35,'rgba(0,0,0,0)');
|
||||
shadow.addColorStop(.7,'rgba(0,0,0,.3)');shadow.addColorStop(1,'rgba(0,0,0,.55)');
|
||||
ctx.fillStyle=shadow;ctx.fillRect(x-r,y-r,r*2,r*2);
|
||||
// Atmosphere rim
|
||||
const atmo=ctx.createRadialGradient(x,y,r*.75,x,y,r*1.02);
|
||||
atmo.addColorStop(0,'rgba(0,0,0,0)');atmo.addColorStop(.88,'rgba(200,220,255,0)');
|
||||
atmo.addColorStop(.95,'rgba(200,220,255,.12)');atmo.addColorStop(1,'rgba(200,220,255,.3)');
|
||||
ctx.fillStyle=atmo;ctx.fillRect(x-r,y-r,r*2,r*2);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════
|
||||
// STATE
|
||||
// ═══════════════════════════════════
|
||||
let simTime=new Date('2060-08-11T10:00:00Z');
|
||||
const TMN=new Date('2050-01-01T00:00Z').getTime(),TMX=new Date('2070-01-01T00:00Z').getTime(),TR=TMX-TMN;
|
||||
let rT=simTime.getTime(),tT=simTime.getTime(),af=null,style='realistic';
|
||||
let vs=[{tp:'solar_system',ck:'sun'}],hv=null,dg=false,dS=null,dL=null,dd=false,vO={x:0,y:0},zm=1;
|
||||
|
||||
// ═══════════════════════════════════
|
||||
// CANVAS
|
||||
// ═══════════════════════════════════
|
||||
const cv=document.getElementById('map-canvas'),ctx=cv.getContext('2d'),ct=document.getElementById('map-container');
|
||||
function rs(){const r=ct.getBoundingClientRect(),d=window.devicePixelRatio||1;cv.width=r.width*d;cv.height=r.height*d;cv.style.width=r.width+'px';cv.style.height=r.height+'px';stars=null}
|
||||
window.addEventListener('resize',()=>{rs();render()});rs();
|
||||
|
||||
function or(ox,oy,rad){ctx.beginPath();ctx.arc(ox,oy,rad,0,2*Math.PI);ctx.strokeStyle='rgba(148,163,184,.12)';ctx.lineWidth=.5;ctx.setLineDash([4,8]);ctx.stroke();ctx.setLineDash([])}
|
||||
function db(x,y,r,k,lb){
|
||||
const b=fv(k);if(!b)return;
|
||||
const isStar=b.t==='star';
|
||||
const img=tex(k);
|
||||
|
||||
if(img){
|
||||
ctx.save();ctx.beginPath();ctx.arc(x,y,r,0,Math.PI*2);ctx.clip();
|
||||
ctx.drawImage(img,x-r,y-r,r*2,r*2);
|
||||
ctx.restore();
|
||||
shadeSphere(ctx,x,y,r,isStar);
|
||||
}else{
|
||||
const bc=b.c||'#888';
|
||||
if(isStar){
|
||||
const glow=ctx.createRadialGradient(x,y,r*.3,x,y,r*2.5);glow.addColorStop(0,bc+'50');glow.addColorStop(.5,bc+'20');glow.addColorStop(1,bc+'00');ctx.fillStyle=glow;ctx.beginPath();ctx.arc(x,y,r*2.5,0,Math.PI*2);ctx.fill();
|
||||
}
|
||||
const g=ctx.createRadialGradient(x-r*.25,y-r*.3,0,x,y,r);
|
||||
g.addColorStop(0,'#ffffff');g.addColorStop(.25,bc);g.addColorStop(1,b.c2||'#333');
|
||||
ctx.fillStyle=g;ctx.beginPath();ctx.arc(x,y,r,0,Math.PI*2);ctx.fill();
|
||||
shadeSphere(ctx,x,y,r,isStar);
|
||||
}
|
||||
|
||||
if(k==='saturn'&&r>6){
|
||||
ctx.save();
|
||||
ctx.strokeStyle='rgba(200,180,140,.3)';ctx.lineWidth=Math.max(1,r*.06);
|
||||
ctx.beginPath();ctx.ellipse(x,y,r*1.5,r*.18,0,0,Math.PI*2);ctx.stroke();
|
||||
ctx.strokeStyle='rgba(200,180,140,.15)';ctx.lineWidth=Math.max(1,r*.04);
|
||||
ctx.beginPath();ctx.ellipse(x,y,r*1.8,r*.28,0,0,Math.PI*2);ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
ctx.fillStyle='#e2e8f0';ctx.font=`${Math.max(8.5,r*.48)}px system-ui,sans-serif`;ctx.textAlign='center';ctx.fillText(lb,x,y-r-5);
|
||||
b._x=x;b._y=y;b._r=r
|
||||
}
|
||||
|
||||
function renderSS(cx,cy,tm){
|
||||
const jv=jd(new Date(tm));
|
||||
for(const k of SO){const rad=Math.pow(ORB[k].a,.4)*100*zm;or(cx,cy,rad)}
|
||||
db(cx,cy,BD.sun.r*zm,'sun','Sun');
|
||||
for(const k of SO){const b=BD[k],p=helio(k,jv);db(cx+p.x*zm,cy-p.y*zm,b.r*zm,k,b.n)}
|
||||
}
|
||||
function renderPS(ck,cx,cy,tm){
|
||||
const c=BD[ck];if(!c)return;const jv=jd(new Date(tm)),pr=c.fr*zm;
|
||||
db(cx,cy,pr,ck,c.n);
|
||||
if(c.moons)for(const[mk,mb]of Object.entries(c.moons)){const od=mb.orb*zm*1.8;or(cx,cy,od);const an=(2*Math.PI*((jv-J2000_JD)%mb.P))/mb.P;db(cx+od*Math.cos(an),cy-od*Math.sin(an),mb.r*2.2*zm,mk,mb.n)}
|
||||
}
|
||||
function renderSB(ck,cx,cy,tm){const b=fv(ck);if(!b)return;const mx=Math.min(cx,cy)*.52,baseR=b.fr?b.fr*3:(b.r||4)*15,rr=Math.min(baseR*zm,mx);db(cx,cy,rr,ck,b.n)}
|
||||
|
||||
function render(){
|
||||
const dpr=window.devicePixelRatio||1,W=cv.width/dpr,H=cv.height/dpr;
|
||||
ctx.save();ctx.setTransform(dpr,0,0,dpr,0,0);
|
||||
if(!stars||stars.width!==cv.width||stars.height!==cv.height)stars=mkStars(cv.width,cv.height);
|
||||
ctx.drawImage(stars,0,0,W,H);
|
||||
const v=vs[vs.length-1],cX=W/2+vO.x,cY=H/2+vO.y,tm=rT;
|
||||
if(v.tp==='solar_system')renderSS(cX,cY,tm);else if(v.tp==='planet_system')renderPS(v.ck,cX,cY,tm);else renderSB(v.ck,cX,cY,tm);
|
||||
if(hv){const b=fv(hv);if(b&&b._x!=null){ctx.beginPath();ctx.arc(b._x,b._y,(b._r||4)+5,0,Math.PI*2);ctx.strokeStyle='rgba(59,130,246,.7)';ctx.lineWidth=2;ctx.stroke()}}
|
||||
ctx.restore();uip(v)
|
||||
}
|
||||
function fv(k){if(BD[k])return BD[k];for(const bk of Object.keys(BD)){const b=BD[bk];if(b.moons&&b.moons[k])return b.moons[k]}return null}
|
||||
|
||||
// ═══════════════════════════════════
|
||||
// PANEL
|
||||
// ═══════════════════════════════════
|
||||
function uip(v){
|
||||
let sk,sn;if(v.tp==='solar_system'){sk='solar_system';sn='Solar System'}else{sk=v.ck;const b=BD[v.ck];sn=b?b.n+(v.tp==='planet_system'?' System':''):v.ck}
|
||||
document.getElementById('panel-title').textContent=sn;document.getElementById('panel-scope').textContent=sn;document.getElementById('scope-label').textContent=sn;document.getElementById('scope-indicator').textContent=sn;
|
||||
const as=MOCK_ASSETS.filter(a=>a.sc.includes(sk)),ad=document.getElementById('panel-assets');
|
||||
ad.innerHTML=as.length?as.map(a=>`<div class="asset-row"><span class="name">${a.n}</span><span class="loc">${a.loc}</span></div>`).join(''):'<div style="color:var(--muted);font-size:.76rem">No assets in scope</div>';
|
||||
document.getElementById('panel-mission-count').textContent=as.filter(a=>a.loc!=='—'&&!a.loc.includes('Surface')).length;
|
||||
const ns=new Date(rT).toISOString().slice(0,10),up=MOCK_EVENTS.filter(e=>e.d>=ns).slice(0,5),ed=document.getElementById('panel-events');
|
||||
ed.innerHTML=up.length?up.map(e=>`<div class="event-row"><span class="date">${e.d}</span> — ${e.t}</div>`).join(''):'<div style="color:var(--muted);font-size:.76rem">No upcoming events</div>'
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════
|
||||
// CLICK (drag threshold 6px)
|
||||
// ═══════════════════════════════════
|
||||
function ht(mx,my){
|
||||
const v=vs[vs.length-1],cs=[];
|
||||
if(v.tp==='solar_system'){cs.push({k:'sun'});for(const k of SO)cs.push({k})}
|
||||
else if(v.tp==='planet_system'){const c=BD[v.ck];if(c){cs.push({k:v.ck});if(c.moons)for(const mk of Object.keys(c.moons))cs.push({k:mk})}}
|
||||
else{const b=BD[v.ck];if(b)cs.push({k:v.ck})}
|
||||
for(const{k}of cs){const b=fv(k);if(!b||b._x==null)continue;const dx=mx-b._x,dy=my-b._y,hr=(b._r||4)+7;if(dx*dx+dy*dy<=hr*hr)return k}return null
|
||||
}
|
||||
cv.addEventListener('mousedown',e=>{dg=true;dS={x:e.clientX,y:e.clientY};dL={x:e.clientX,y:e.clientY};dd=false});
|
||||
window.addEventListener('mousemove',e=>{if(!dg||!dL)return;const dx=e.clientX-dS.x,dy=e.clientY-dS.y;if(Math.abs(dx)>6||Math.abs(dy)>6)dd=true;if(dd){vO.x+=e.clientX-dL.x;vO.y+=e.clientY-dL.y;dL={x:e.clientX,y:e.clientY};cv.style.cursor='grabbing';render()}});
|
||||
window.addEventListener('mouseup',()=>{dg=false;dS=null;dL=null;cv.style.cursor=hv?'pointer':'grab'});
|
||||
cv.addEventListener('click',e=>{if(dd){dd=false;return}const r=cv.getBoundingClientRect(),t=ht(e.clientX-r.left,e.clientY-r.top);if(!t)return;const v=vs[vs.length-1];if(v.tp==='solar_system'){const b=BD[t];vs.push({tp:b&&b.moons?'planet_system':'single_body',ck:t})}else if(v.tp==='planet_system'){if(t===v.ck)vs.push({tp:'single_body',ck:t});else vs.push({tp:'single_body',ck:t})}vO={x:0,y:0};ubb();render()});
|
||||
cv.addEventListener('mousemove',e=>{if(dd)return;const r=cv.getBoundingClientRect(),t=ht(e.clientX-r.left,e.clientY-r.top);if(t!==hv){hv=t;cv.style.cursor=t?'pointer':(dg?'grabbing':'grab');render()}});
|
||||
cv.addEventListener('wheel',e=>{e.preventDefault();zm=Math.max(.3,Math.min(4,zm*(e.deltaY>0?.9:1.1)));render()},{passive:false});
|
||||
function goBack(){if(vs.length>1)vs.pop();vO={x:0,y:0};zm=1;ubb();render()}
|
||||
function ubb(){document.getElementById('back-btn').classList.toggle('visible',vs.length>1)}
|
||||
function setStyle(s){style=s;document.getElementById('btn-flat').classList.toggle('active',s==='flat');document.getElementById('btn-realistic').classList.toggle('active',s==='realistic');render()}
|
||||
|
||||
// ═══════════════════════════════════
|
||||
// TIME
|
||||
// ═══════════════════════════════════
|
||||
function fmt(ms){const d=new Date(ms);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 uti(){const i=document.getElementById('sim-time-input');if(i&&document.activeElement!==i)i.value=fmt(simTime.getTime());const s=document.getElementById('time-slider');if(s&&document.activeElement!==s)s.value=Math.round(((simTime.getTime()-TMN)/TR)*1000)}
|
||||
function st(ms){tT=Math.min(TMX,Math.max(TMN,ms));ani()}
|
||||
function sh(d){st(simTime.getTime()+d*DAY)}
|
||||
function rst(){st(new Date('2060-08-11T10:00:00Z').getTime())}
|
||||
function ani(){if(af)cancelAnimationFrame(af);function step(){const d=tT-rT;if(Math.abs(d)<DAY*.3){rT=tT;simTime=new Date(tT);render();uti();af=null;return}rT+=d*.18;simTime=new Date(Math.round(rT));render();uti();af=requestAnimationFrame(step)}af=requestAnimationFrame(step)}
|
||||
document.getElementById('time-slider').addEventListener('input',()=>{st(TMN+(parseInt(document.getElementById('time-slider').value)/1000)*TR)});
|
||||
document.getElementById('sim-time-input').addEventListener('keydown',e=>{if(e.key==='Enter'){const p=new Date(document.getElementById('sim-time-input').value.trim()+'Z');if(!isNaN(p.getTime()))st(p.getTime());else{const i=document.getElementById('sim-time-input');i.style.borderColor='var(--danger)';setTimeout(()=>{i.style.borderColor=''},800)}}});
|
||||
|
||||
// ═══════════════════════════════════
|
||||
rT=simTime.getTime();tT=simTime.getTime();uti();ubb();loadTextures();render();
|
||||
window.addEventListener('resize',()=>{rs();render()});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 79 KiB |
@@ -0,0 +1,140 @@
|
||||
"""Test the location board demo click functionality using Playwright."""
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
DEMO_PATH = Path(__file__).resolve().parent / "location_board_demo.html"
|
||||
FILE_URL = f"file:///{DEMO_PATH.as_posix()}"
|
||||
|
||||
|
||||
async def main():
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(headless=True)
|
||||
page = await browser.new_page(viewport={"width": 1400, "height": 900})
|
||||
await page.goto(FILE_URL, wait_until="commit", timeout=10000)
|
||||
await page.wait_for_timeout(3000) # let canvas render (skip font loading)
|
||||
|
||||
# --- Test 1: Check initial state ---
|
||||
scope = await page.text_content("#scope-indicator")
|
||||
print(f"Test 1 - Initial scope: {scope}")
|
||||
|
||||
# --- Test 2: Get canvas bounding box and try clicking a planet ---
|
||||
canvas = page.locator("#map-canvas")
|
||||
box = await canvas.bounding_box()
|
||||
if not box:
|
||||
print("ERROR: Canvas not found!")
|
||||
await browser.close()
|
||||
return
|
||||
|
||||
print(f"Canvas: x={box['x']:.0f} y={box['y']:.0f} w={box['width']:.0f} h={box['height']:.0f}")
|
||||
|
||||
# The solar system view has the sun at center of canvas
|
||||
# Planets orbit around it. Let's log JS state to see where bodies are.
|
||||
body_positions = await page.evaluate("""
|
||||
() => {
|
||||
const result = [];
|
||||
const keys = ['sun','mercury','venus','earth','mars','jupiter','saturn','uranus','neptune','pluto'];
|
||||
for (const k of keys) {
|
||||
const b = BD[k];
|
||||
if (b) result.push({
|
||||
key: k,
|
||||
name: b.n,
|
||||
sx: b._x,
|
||||
sy: b._y,
|
||||
r: b.r,
|
||||
hasMoons: (b.moons && typeof b.moons==='object') ? Object.keys(b.moons).join(',') : 'none'
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
""")
|
||||
print("\nBody positions (CSS pixels from canvas top-left):")
|
||||
for bp in body_positions:
|
||||
sx = bp['sx']
|
||||
sy = bp['sy']
|
||||
if sx is not None and sy is not None:
|
||||
# Convert to page coordinates (add canvas offset)
|
||||
px = sx + box['x']
|
||||
py = sy + box['y']
|
||||
print(f" {bp['name']:12s} screen=({sx:6.1f},{sy:6.1f}) page=({px:6.1f},{py:6.1f}) r={bp['r']} moons={bp['hasMoons']}")
|
||||
else:
|
||||
print(f" {bp['name']:12s} screen=(undefined) - NOT RENDERED")
|
||||
|
||||
# --- Test 3: Try clicking Jupiter ---
|
||||
jupiter = [b for b in body_positions if b['key'] == 'jupiter']
|
||||
if jupiter and jupiter[0]['sx'] is not None:
|
||||
jx = jupiter[0]['sx'] + box['x']
|
||||
jy = jupiter[0]['sy'] + box['y']
|
||||
jr = jupiter[0]['r'] + 5 # hit radius
|
||||
print(f"\nTest 3 - Clicking Jupiter at page ({jx:.0f}, {jy:.0f}) r={jr}")
|
||||
|
||||
# Take pre-click screenshot
|
||||
await page.screenshot(path=str(DEMO_PATH.parent / "pre_click.png"))
|
||||
print(" Screenshot: pre_click.png")
|
||||
|
||||
# Click
|
||||
await page.mouse.click(jx, jy)
|
||||
await page.wait_for_timeout(500)
|
||||
|
||||
scope_after = await page.text_content("#scope-indicator")
|
||||
back_visible = await page.evaluate("() => document.getElementById('back-btn').classList.contains('visible')")
|
||||
print(f" After click - scope: {scope_after}")
|
||||
print(f" Back button visible: {back_visible}")
|
||||
print(f" View stack: {await page.evaluate('() => JSON.stringify(viewStack)')}")
|
||||
|
||||
await page.screenshot(path=str(DEMO_PATH.parent / "post_click.png"))
|
||||
print(" Screenshot: post_click.png")
|
||||
else:
|
||||
print("\nTest 3 - Jupiter not found in rendered bodies!")
|
||||
|
||||
# --- Test 4: Check JS console for errors ---
|
||||
console_msgs = []
|
||||
page.on("console", lambda msg: console_msgs.append(f"[{msg.type}] {msg.text}"))
|
||||
# Trigger a re-render
|
||||
await page.evaluate("() => { render(); }")
|
||||
await page.wait_for_timeout(300)
|
||||
errors = [m for m in console_msgs if 'error' in m.lower() or 'Error' in m]
|
||||
if errors:
|
||||
print(f"\nJS errors: {errors}")
|
||||
else:
|
||||
print("\nNo JS errors detected")
|
||||
|
||||
# --- Test 5: Check didDrag state and event flow ---
|
||||
print("\n--- Event Debug ---")
|
||||
# Inject debug listener
|
||||
await page.evaluate("""
|
||||
() => {
|
||||
const canvas = document.getElementById('map-canvas');
|
||||
canvas.addEventListener('click', (e) => {
|
||||
console.log('CLICK on canvas at', e.clientX, e.clientY, 'didDrag=', window._didDrag || didDrag);
|
||||
}, true); // capture phase
|
||||
|
||||
// Store for inspection
|
||||
window._debugClick = (x, y) => {
|
||||
const r = canvas.getBoundingClientRect();
|
||||
const t = hitTest(x - r.left, y - r.top);
|
||||
console.log('hitTest result:', t, 'didDrag=', didDrag, 'dragOn=', dragOn);
|
||||
return t;
|
||||
};
|
||||
}
|
||||
""")
|
||||
|
||||
# Try click with explicit coordinates
|
||||
if jupiter and jupiter[0]['sx'] is not None:
|
||||
result = await page.evaluate(f"() => window._debugClick({jx - box['x']}, {jy - box['y']})")
|
||||
print(f"hitTest at Jupiter position result: {result}")
|
||||
|
||||
# Also try clicking center of canvas minus offset
|
||||
cx = box['width'] / 2
|
||||
cy = box['height'] / 2
|
||||
for offset_y in [0, -20, -50, -100, 50, 100]:
|
||||
test_x = cx
|
||||
test_y = cy + offset_y
|
||||
r = await page.evaluate(f"() => window._debugClick({test_x}, {test_y})")
|
||||
print(f" hitTest at canvas({test_x:.0f}, {test_y:.0f}): {r}")
|
||||
|
||||
await browser.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 87 KiB |
@@ -0,0 +1,212 @@
|
||||
{
|
||||
"total": 41,
|
||||
"passed": 41,
|
||||
"failed": 0,
|
||||
"results": [
|
||||
{
|
||||
"name": "1.1 侧边栏导航",
|
||||
"ok": true,
|
||||
"detail": "11 items"
|
||||
},
|
||||
{
|
||||
"name": "1.2 侧边栏默认展开",
|
||||
"ok": true,
|
||||
"detail": "2 expanded"
|
||||
},
|
||||
{
|
||||
"name": "1.3 当前页高亮",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "1.4 默认深色模式",
|
||||
"ok": true,
|
||||
"detail": "theme=dark"
|
||||
},
|
||||
{
|
||||
"name": "1.5 浅色模式",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "1.6 深色模式切换回",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "1.7 模拟时间显示",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "1.12 Modal打开",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "1.14 Cancel关闭",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "1.15 ESC关闭",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "2.1 标题",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "2.2 指标卡片",
|
||||
"ok": true,
|
||||
"detail": "3 cards"
|
||||
},
|
||||
{
|
||||
"name": "2.3 真实数据",
|
||||
"ok": true,
|
||||
"detail": "count=14"
|
||||
},
|
||||
{
|
||||
"name": "2.4 状态看板",
|
||||
"ok": true,
|
||||
"detail": "3 groups"
|
||||
},
|
||||
{
|
||||
"name": "2.6 资产链接",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "3.1 列表渲染",
|
||||
"ok": true,
|
||||
"detail": "14 rows"
|
||||
},
|
||||
{
|
||||
"name": "3.2 搜索",
|
||||
"ok": true,
|
||||
"detail": "14->1"
|
||||
},
|
||||
{
|
||||
"name": "4.1 详情渲染",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "4.2 信息卡片",
|
||||
"ok": true,
|
||||
"detail": "4 cards"
|
||||
},
|
||||
{
|
||||
"name": "4.5 日志列表",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "5.1 Dock按钮",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "5.2 Modal内容",
|
||||
"ok": true,
|
||||
"detail": "15 options"
|
||||
},
|
||||
{
|
||||
"name": "5.6 Cancel",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "6.1 State Interval",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "6.5 Node添加",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "6.10 Modal关闭",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "7.1 Canvas",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "7.2 范围",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "8.1 Canvas",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "8.2 刻度行",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "8.3 段",
|
||||
"ok": true,
|
||||
"detail": "116 segments"
|
||||
},
|
||||
{
|
||||
"name": "8.4 事件",
|
||||
"ok": true,
|
||||
"detail": "238 events"
|
||||
},
|
||||
{
|
||||
"name": "8.5 资产链接",
|
||||
"ok": true,
|
||||
"detail": "14 links"
|
||||
},
|
||||
{
|
||||
"name": "8.6 JSON数据",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "8.7 滚轮缩放",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "9.engines",
|
||||
"ok": true,
|
||||
"detail": "30 rows"
|
||||
},
|
||||
{
|
||||
"name": "9.comms",
|
||||
"ok": true,
|
||||
"detail": "30 rows"
|
||||
},
|
||||
{
|
||||
"name": "9.tanks",
|
||||
"ok": true,
|
||||
"detail": "26 rows"
|
||||
},
|
||||
{
|
||||
"name": "9.vehicles",
|
||||
"ok": true,
|
||||
"detail": "10 rows"
|
||||
},
|
||||
{
|
||||
"name": "10.1 渲染",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "JS Errors",
|
||||
"ok": true,
|
||||
"detail": "0 errors"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 63 KiB |