feat: add simulation settings and location board redesign
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,79 +1,286 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div style="display:flex;flex:1;overflow:hidden;position:absolute;inset:0;top:var(--topbar-h);left:var(--sidebar-w)">
|
||||
<!-- Canvas Map -->
|
||||
<div id="map-ct" style="flex:1;position:relative;display:flex;align-items:center;justify-content:center;background:#0B0B10;cursor:grab;overflow:hidden">
|
||||
<canvas id="map-cv"></canvas>
|
||||
<button id="bk-btn" onclick="mapGoBack()">← Back</button>
|
||||
<span id="scope-ind">Solar System</span>
|
||||
</div>
|
||||
<section id="ksp-location-board" aria-label="太阳系地点板">
|
||||
<header class="location-toolbar">
|
||||
<nav class="location-breadcrumbs" id="location-breadcrumbs" aria-label="地图层级"></nav>
|
||||
<div class="location-summary">
|
||||
<label class="transfer-toggle" for="transfer-toggle">
|
||||
<input id="transfer-toggle" type="checkbox">
|
||||
<span>全部转移轨迹</span>
|
||||
</label>
|
||||
<span id="scope-count">0 assets</span>
|
||||
<span class="live-status"><i></i>实时星历</span>
|
||||
<button class="location-reset" id="location-reset" type="button">重置</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Info Panel -->
|
||||
<div id="map-info" style="width:280px;background:var(--surface);border-left:1px solid var(--border);padding:16px;overflow-y:auto;display:flex;flex-direction:column;gap:14px;flex-shrink:0">
|
||||
<h2 id="mi-title" style="font-family:var(--font-data);font-size:.85rem;font-weight:600;padding-bottom:5px;border-bottom:1px solid var(--border)">Solar System</h2>
|
||||
<div><h3 style="font-family:var(--font-data);font-size:.6rem;text-transform:uppercase;color:var(--muted);letter-spacing:.05em;margin-bottom:3px">Scope</h3><div id="mi-scope" style="font-size:.88rem;font-weight:500">Solar System</div></div>
|
||||
<div><h3 style="font-family:var(--font-data);font-size:.6rem;text-transform:uppercase;color:var(--muted);letter-spacing:.05em;margin-bottom:3px">Assets in Scope</h3><div id="mi-assets"></div></div>
|
||||
<div><h3 style="font-family:var(--font-data);font-size:.6rem;text-transform:uppercase;color:var(--muted);letter-spacing:.05em;margin-bottom:3px">Ongoing Missions</h3><div style="font-family:var(--font-data);font-size:1.3rem;font-weight:600;color:var(--accent)" id="mi-miss">{{ ongoing_count }}</div></div>
|
||||
<div><h3 style="font-family:var(--font-data);font-size:.6rem;text-transform:uppercase;color:var(--muted);letter-spacing:.05em;margin-bottom:3px">Upcoming Events</h3><div id="mi-events"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="location-layout">
|
||||
<main class="location-map" id="location-map">
|
||||
<svg id="space-map" viewBox="0 0 1000 620" preserveAspectRatio="xMidYMid meet" role="img" aria-labelledby="map-title map-desc">
|
||||
<title id="map-title">太阳系资产位置地图</title>
|
||||
<desc id="map-desc">从太阳系全局钻取到行星系统、行星和卫星,并查看实际转移航路。</desc>
|
||||
<g id="orbit-layer"></g>
|
||||
<g id="route-layer"></g>
|
||||
<g id="asset-layer"></g>
|
||||
</svg>
|
||||
<div class="body-layer" id="body-layer"></div>
|
||||
<div class="map-caption"><span id="view-mode">SOLAR SYSTEM</span><b id="map-caption">选择天体或转移资产</b></div>
|
||||
<div class="map-legend" aria-label="图例">
|
||||
<span><i class="asset-dot"></i>资产位置</span>
|
||||
<span><i class="progress-line"></i>已完成航程</span>
|
||||
<span><i class="remaining-line"></i>剩余航程</span>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Pass asset data from backend to JS
|
||||
const LOC_ASSETS = {{ asset_positions_json|safe }};
|
||||
const LOC_EVENTS = {{ upcoming_events|tojson|safe }};
|
||||
</script>
|
||||
<aside class="asset-panel">
|
||||
<div class="asset-panel-head">
|
||||
<div><span>ASSETS IN SCOPE</span><h2 id="scope-title">Solar System</h2></div>
|
||||
<b id="panel-count">0</b>
|
||||
</div>
|
||||
<div class="asset-list" id="asset-list"></div>
|
||||
</aside>
|
||||
|
||||
<aside class="asset-drawer" id="asset-drawer" aria-hidden="true">
|
||||
<button class="drawer-close" id="drawer-close" type="button" aria-label="关闭详情">×</button>
|
||||
<span class="drawer-eyebrow">ASSET DETAIL</span>
|
||||
<h2 id="drawer-name"></h2>
|
||||
<div class="drawer-fields" id="drawer-fields"></div>
|
||||
<a class="drawer-link" id="drawer-link" href="#">打开资产详情页</a>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script id="location-assets-data" type="application/json">{{ asset_positions|tojson }}</script>
|
||||
|
||||
<style>
|
||||
#ksp-location-board{position:absolute;inset:0;display:grid;grid-template-rows:auto minmax(0,1fr);overflow:hidden;background:var(--bg);color:var(--text);font-size:clamp(12px,.28vw + 8px,22px)}
|
||||
#ksp-location-board button,#ksp-location-board a{font:inherit}
|
||||
.location-toolbar{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:48px;padding:8px clamp(14px,1vw,30px);border-bottom:1px solid var(--border);background:var(--surface)}
|
||||
.location-breadcrumbs,.location-summary{display:flex;align-items:center;gap:9px;min-width:0}
|
||||
.location-breadcrumbs button{padding:0;border:0;background:transparent;color:var(--muted);cursor:pointer}
|
||||
.location-breadcrumbs button.current{color:var(--text);font-weight:600}.location-breadcrumbs .sep{color:var(--muted)}
|
||||
.location-summary{color:var(--muted);white-space:nowrap}.live-status{display:flex;align-items:center;gap:5px}.live-status i{width:7px;height:7px;border-radius:50%;background:var(--amber)}
|
||||
.transfer-toggle{display:flex;align-items:center;gap:7px;cursor:pointer;color:var(--text)}.transfer-toggle input{accent-color:var(--accent)}
|
||||
.location-reset{padding:4px 9px;border:1px solid var(--border);border-radius:4px;background:transparent;color:var(--muted);cursor:pointer}.location-reset:hover{color:var(--text);background:var(--surface2)}
|
||||
.location-layout{position:relative;display:grid;grid-template-columns:minmax(0,1fr) clamp(270px,20vw,540px);min-width:0;min-height:0}
|
||||
.location-map{position:relative;min-width:0;min-height:0;overflow:hidden;background:radial-gradient(circle at 50% 50%,rgba(59,130,246,.09),transparent 44%),var(--bg)}
|
||||
#space-map{display:block;width:100%;height:100%}.body-layer{position:absolute;inset:0;pointer-events:none}
|
||||
.asset-panel{display:grid;grid-template-rows:auto minmax(0,1fr);min-height:0;padding:clamp(14px,1vw,28px);border-left:1px solid var(--border);background:var(--surface)}
|
||||
.asset-panel-head{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;padding-bottom:10px;border-bottom:1px solid var(--border)}
|
||||
.asset-panel-head span,.drawer-eyebrow{color:var(--muted);font-family:var(--font-data);font-size:.7em;letter-spacing:.08em}.asset-panel-head h2{margin-top:4px;font-size:1.2em}.asset-panel-head b{padding:4px 8px;background:rgba(59,130,246,.12);color:var(--accent);font-family:var(--font-data)}
|
||||
.asset-list{min-height:0;overflow-y:auto}.asset-button{display:flex;align-items:center;gap:9px;width:100%;padding:clamp(9px,.55vw,18px) 3px;border:0;border-bottom:1px solid var(--border);background:transparent;color:var(--text);text-align:left;cursor:pointer}.asset-button::before{content:"";width:7px;height:7px;border-radius:50%;background:var(--amber);flex:0 0 auto}.asset-button.planned::before{background:var(--purple)}.asset-button:hover,.asset-button.selected{background:rgba(59,130,246,.08)}
|
||||
.asset-empty{padding:18px 2px;color:var(--muted)}
|
||||
.body-button{--planet-size:16px;position:absolute;transform:translate(-50%,-50%);display:block;width:var(--planet-size);height:var(--planet-size);padding:0;border:0;background:transparent;color:var(--text);pointer-events:auto;cursor:pointer;overflow:visible}
|
||||
.planet-disc{display:block;width:100%;height:100%;border:1px solid var(--muted);border-radius:50%;background:var(--surface2)}.body-button.sun .planet-disc{border-color:var(--text);background:#74b4ec;box-shadow:0 0 26px rgba(116,180,236,.42)}.body-button.minor .planet-disc{border-style:dashed;background:rgba(148,163,184,.12)}.body-button:hover .planet-disc,.body-button:focus-visible .planet-disc{outline:2px solid var(--accent);outline-offset:3px}
|
||||
.body-label{position:absolute;top:calc(100% + 5px);left:50%;transform:translateX(-50%);color:var(--muted);white-space:nowrap;pointer-events:none}
|
||||
.map-caption{position:absolute;top:16px;left:18px;display:flex;align-items:center;gap:9px;max-width:70%;pointer-events:none}.map-caption span{padding:5px 8px;background:rgba(59,130,246,.12);color:#7ab8f6;font-family:var(--font-data)}.map-caption b{font-weight:500}
|
||||
.map-legend{position:absolute;bottom:16px;left:18px;display:flex;gap:14px;padding:8px 10px;border:1px solid var(--border);background:rgba(19,19,24,.88);color:var(--text)}.map-legend span{display:flex;align-items:center;gap:6px}.asset-dot{width:7px;height:7px;border-radius:50%;background:var(--amber)}.progress-line,.remaining-line{display:inline-block;width:24px;border-top:2px solid #72b4f4}.remaining-line{border-top:1px dashed var(--muted)}
|
||||
.orbit{fill:none;stroke:rgba(148,163,184,.18);stroke-width:1.1}.orbit.strong{stroke:rgba(248,250,252,.48);stroke-width:1.35}.orbit.minor{stroke-dasharray:3 7;opacity:.65}
|
||||
.transfer-track{fill:none;stroke:rgba(148,163,184,.38);stroke-width:1.25;stroke-dasharray:4 8}.transfer-progress{fill:none;stroke:#72b4f4;stroke-width:2.25}.route-endpoint{fill:var(--bg);stroke:#72b4f4;stroke-width:1.6}.route-vessel-dot{fill:var(--bg);stroke:#72b4f4;stroke-width:2.5}.route-vessel-heading{stroke:#72b4f4;stroke-width:2;stroke-linecap:round}.route-label-leader{stroke:var(--muted);stroke-width:1;opacity:.7}.route-label-title,.route-label-meta{font-family:var(--font-ui);paint-order:stroke;stroke:var(--bg);stroke-linejoin:round}.route-label-title{fill:var(--text);font-size:12px;font-weight:600;stroke-width:3}.route-label-meta{fill:var(--muted);font-size:10px;stroke-width:3}.route-group.hidden{display:none}
|
||||
.map-asset{fill:var(--amber);stroke:var(--bg);stroke-width:2}.map-asset-label{fill:var(--text);font-size:11px;font-family:var(--font-ui);paint-order:stroke;stroke:var(--bg);stroke-width:3;stroke-linejoin:round}
|
||||
.asset-drawer{--panel-width:clamp(270px,20vw,540px);position:absolute;z-index:8;top:0;right:var(--panel-width);bottom:0;width:clamp(320px,23vw,620px);display:flex;flex-direction:column;gap:clamp(13px,.8vw,24px);padding:clamp(18px,1.2vw,36px);border-left:1px solid var(--border);background:var(--surface2);transform:translateX(calc(100% + var(--panel-width)));transition:transform .18s ease;pointer-events:none}.asset-drawer.open{transform:translateX(0);pointer-events:auto}.drawer-close{align-self:flex-end;width:30px;height:30px;border:1px solid var(--border);border-radius:4px;background:transparent;color:var(--muted);font-size:20px;cursor:pointer}.asset-drawer h2{font-size:1.4em}.drawer-fields{display:grid;gap:10px}.drawer-field{padding-top:9px;border-top:1px solid var(--border)}.drawer-field span,.drawer-field strong{display:block}.drawer-field span{margin-bottom:3px;color:var(--muted);font-size:.78em}.drawer-field strong{font-weight:500;overflow-wrap:anywhere}.drawer-link{margin-top:auto;padding:9px 12px;border-radius:4px;background:var(--accent);color:#fff;text-align:center}.drawer-link:hover{text-decoration:none;filter:brightness(1.08)}
|
||||
@media(max-width:900px){.location-layout{grid-template-columns:minmax(0,1fr) 230px}.asset-drawer{--panel-width:230px;right:230px;width:300px}.live-status{display:none}.map-legend{display:none}}
|
||||
@media(max-width:680px){.location-toolbar{align-items:flex-start;flex-direction:column}.location-summary{width:100%;overflow-x:auto}.location-layout{grid-template-columns:minmax(0,1fr) 180px}.asset-drawer{--panel-width:180px;right:180px;width:280px}.map-caption{max-width:90%}}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// ═══ Keplerian Orbital Mechanics ═══
|
||||
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}}
|
||||
(() => {
|
||||
const root = document.getElementById('ksp-location-board');
|
||||
const assets = JSON.parse(document.getElementById('location-assets-data').textContent);
|
||||
const ns = 'http://www.w3.org/2000/svg';
|
||||
const layers = {
|
||||
orbit: root.querySelector('#orbit-layer'),
|
||||
route: root.querySelector('#route-layer'),
|
||||
asset: root.querySelector('#asset-layer')
|
||||
};
|
||||
const systems = {
|
||||
mercury:{name:'Mercury',moons:[]}, venus:{name:'Venus',moons:[]}, earth:{name:'Earth',moons:['moon']},
|
||||
mars:{name:'Mars',moons:['phobos','deimos']}, ceres:{name:'Ceres',moons:[]}, vesta:{name:'Vesta',moons:[]},
|
||||
jupiter:{name:'Jupiter',moons:['io','europa','ganymede','callisto']},
|
||||
saturn:{name:'Saturn',moons:['mimas','enceladus','tethys','dione','rhea','titan','iapetus']},
|
||||
uranus:{name:'Uranus',moons:['miranda','ariel','umbriel','titania','oberon']},
|
||||
neptune:{name:'Neptune',moons:['triton']}, pluto:{name:'Pluto',moons:['charon']}
|
||||
};
|
||||
const bodyNames = {sun:'Sun',mercury:'Mercury',venus:'Venus',earth:'Earth',moon:'Moon',mars:'Mars',phobos:'Phobos',deimos:'Deimos',ceres:'Ceres',vesta:'Vesta',jupiter:'Jupiter',io:'Io',europa:'Europa',ganymede:'Ganymede',callisto:'Callisto',saturn:'Saturn',mimas:'Mimas',enceladus:'Enceladus',tethys:'Tethys',dione:'Dione',rhea:'Rhea',titan:'Titan',iapetus:'Iapetus',uranus:'Uranus',miranda:'Miranda',ariel:'Ariel',umbriel:'Umbriel',titania:'Titania',oberon:'Oberon',neptune:'Neptune',triton:'Triton',pluto:'Pluto',charon:'Charon',unknown:'Unknown'};
|
||||
const moonSystems = {moon:'earth',phobos:'mars',deimos:'mars',io:'jupiter',europa:'jupiter',ganymede:'jupiter',callisto:'jupiter',mimas:'saturn',enceladus:'saturn',tethys:'saturn',dione:'saturn',rhea:'saturn',titan:'saturn',iapetus:'saturn',miranda:'uranus',ariel:'uranus',umbriel:'uranus',titania:'uranus',oberon:'uranus',triton:'neptune',charon:'pluto'};
|
||||
const planets = [
|
||||
{id:'mercury',angle:-35,size:9,orbit:50},{id:'venus',angle:60,size:12,orbit:78},{id:'earth',angle:-22,size:13,orbit:110},{id:'mars',angle:125,size:11,orbit:140},
|
||||
{id:'vesta',angle:-135,size:7,orbit:160,kind:'minor'},{id:'ceres',angle:150,size:8,orbit:176,kind:'minor'},{id:'jupiter',angle:32,size:25,orbit:215},
|
||||
{id:'saturn',angle:210,size:22,orbit:260},{id:'uranus',angle:-28,size:17,orbit:310},{id:'neptune',angle:145,size:17,orbit:360},{id:'pluto',angle:210,size:10,orbit:420,kind:'minor'}
|
||||
];
|
||||
let view = {level:'solar',system:null,body:null};
|
||||
let selectedRoute = null;
|
||||
let showAllRoutes = false;
|
||||
let bodyPoints = {};
|
||||
|
||||
// ═══ Star Map Data ═══
|
||||
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},titan:{n:'Titan',r:3.5,c:'#d4a85c',c2:'#a07830',orb:90,P:15.95}}},uranus:{n:'Uranus',t:'p',r:11,fr:46,c:'#7ec8e3',c2:'#4a90b0',mo:{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}
|
||||
|
||||
// ═══ State ═══
|
||||
let mapVS=[{tp:'solar',ck:'sun'}],mapHv=null,mapDg=false,mapDS=null,mapDL=null,mapDD=false,mapVO={x:0,y:0},mapZm=1;
|
||||
let mapCtx,mapCv,mapStars=null;
|
||||
|
||||
function initMap(){
|
||||
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);
|
||||
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();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,'#fff');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();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='#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('{{ simulation_time_display }}Z'));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);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()}}if(mapHv){const b=mapFV(mapHv);if(b&&b._x!=null){mapCtx.beginPath();mapCtx.arc(mapHv?b._x:0,mapHv?b._y:0,(b._r||4)+5,0,Math.PI*2);mapCtx.strokeStyle='rgba(59,130,246,.7)';mapCtx.lineWidth=2;mapCtx.stroke()}}mapCtx.restore();rMapInfo()}
|
||||
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}document.getElementById('mi-title').textContent=sn;document.getElementById('mi-scope').textContent=sn;const as=LOC_ASSETS.filter(a=>{if(sk==='solar_system')return true;const loc=a.location.toLowerCase();if(sk==='earth')return loc.includes('leo')||loc.includes('earth')||loc.includes('lunar');if(sk==='mars')return loc.includes('mars');if(sk==='jupiter')return loc.includes('jupiter')||loc.includes('europa');if(sk==='saturn')return loc.includes('saturn');if(sk==='uranus')return loc.includes('uranus');if(sk==='neptune')return loc.includes('neptune');if(sk==='mercury')return loc.includes('mercury');if(sk==='venus')return loc.includes('venus');if(sk==='ceres')return loc.includes('ceres');if(sk==='vesta')return loc.includes('vesta');if(sk==='pluto')return loc.includes('pluto');return loc.includes(sk)});document.getElementById('mi-assets').innerHTML=as.length?as.map(a=>`<div style="display:flex;justify-content:space-between;padding:3px 0;border-bottom:1px solid var(--border);font-size:.74rem"><span style="color:var(--accent);cursor:pointer">${a.name}</span><span class="mt" style="font-family:var(--font-data);font-size:.66rem">${a.location}</span></div>`).join(''):'<div style="color:var(--muted);font-size:.72rem">No assets</div>'}
|
||||
function updateMapBack(){document.getElementById('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()}
|
||||
initMap();
|
||||
function svgEl(tag, attrs, text) {
|
||||
const node = document.createElementNS(ns, tag);
|
||||
Object.entries(attrs || {}).forEach(([key, value]) => node.setAttribute(key, String(value)));
|
||||
if (text !== undefined) node.textContent = text;
|
||||
return node;
|
||||
}
|
||||
function clearMap() {
|
||||
Object.values(layers).forEach(layer => layer.replaceChildren());
|
||||
root.querySelector('#body-layer').replaceChildren();
|
||||
bodyPoints = {};
|
||||
}
|
||||
function orbit(cx, cy, rx, ry, strong=false, kind='') {
|
||||
layers.orbit.append(svgEl('ellipse', {cx,cy,rx,ry,class:'orbit '+(strong?'strong ':'')+(kind==='minor'?'minor':'')}));
|
||||
}
|
||||
function planetPoint(planet) {
|
||||
const angle = planet.angle * Math.PI / 180;
|
||||
return {x:500 + Math.cos(angle) * planet.orbit, y:310 + Math.sin(angle) * planet.orbit * .6};
|
||||
}
|
||||
function mapPoint(x,y) {
|
||||
const stage = root.querySelector('#location-map');
|
||||
const width = stage.clientWidth || 1000;
|
||||
const height = stage.clientHeight || 620;
|
||||
const scale = Math.min(width/1000,height/620);
|
||||
return {x:(width-1000*scale)/2+x*scale,y:(height-620*scale)/2+y*scale};
|
||||
}
|
||||
function bodyButton(id, name, x, y, size, sun=false, kind='') {
|
||||
bodyPoints[id] = {x,y};
|
||||
const point = mapPoint(x,y);
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'body-button '+(sun?'sun ':'')+(kind==='minor'?'minor':'');
|
||||
button.style.left = point.x+'px';
|
||||
button.style.top = point.y+'px';
|
||||
button.style.setProperty('--planet-size','clamp('+size+'px,'+(size/10).toFixed(2)+'vw,'+(size*2)+'px)');
|
||||
button.setAttribute('aria-label','打开 '+name);
|
||||
const disc = document.createElement('span'); disc.className = 'planet-disc';
|
||||
const label = document.createElement('span'); label.className = 'body-label'; label.textContent = name;
|
||||
button.append(disc,label);
|
||||
button.addEventListener('click',()=>openBody(id));
|
||||
root.querySelector('#body-layer').append(button);
|
||||
}
|
||||
function systemForBody(body) { return moonSystems[body] || body; }
|
||||
function visibleTransfer(asset) {
|
||||
if (!asset.transfer) return false;
|
||||
if (view.level === 'system') return asset.scope === view.system;
|
||||
return view.level === 'solar' || view.system === 'sun' ? asset.scope === 'sun' : false;
|
||||
}
|
||||
function transferPoint(body) {
|
||||
const system = systemForBody(body);
|
||||
return bodyPoints[body] || bodyPoints[system] || null;
|
||||
}
|
||||
function drawTransfer(asset, index) {
|
||||
if (!visibleTransfer(asset)) return;
|
||||
const from = transferPoint(asset.transfer.from);
|
||||
const to = transferPoint(asset.transfer.to);
|
||||
if (!from || !to) return;
|
||||
const bend = index % 2 ? -1 : 1;
|
||||
const mx = (from.x + to.x) / 2;
|
||||
const my = (from.y + to.y) / 2;
|
||||
const dx = to.x-from.x, dy = to.y-from.y;
|
||||
const length = Math.max(1,Math.hypot(dx,dy));
|
||||
const offset = Math.min(85,length*.24)*bend;
|
||||
const cx = mx-dy/length*offset, cy = my+dx/length*offset;
|
||||
const d = `M${from.x} ${from.y} Q${cx} ${cy} ${to.x} ${to.y}`;
|
||||
const group = svgEl('g',{class:'route-group'+((showAllRoutes||selectedRoute===asset.id)?'':' hidden')});
|
||||
layers.route.append(group);
|
||||
const track = svgEl('path',{d,class:'transfer-track',pathLength:100}); group.append(track);
|
||||
group.append(svgEl('path',{d,class:'transfer-progress',pathLength:100,'stroke-dasharray':asset.transfer.progress+' '+(100-asset.transfer.progress)}));
|
||||
const total = track.getTotalLength();
|
||||
const start = track.getPointAtLength(0), end = track.getPointAtLength(total);
|
||||
const point = track.getPointAtLength(total*asset.transfer.progress/100);
|
||||
const before = track.getPointAtLength(Math.max(0,total*asset.transfer.progress/100-2));
|
||||
const angle = Math.atan2(point.y-before.y,point.x-before.x)*180/Math.PI;
|
||||
group.append(svgEl('circle',{cx:start.x,cy:start.y,r:4,class:'route-endpoint'}));
|
||||
group.append(svgEl('circle',{cx:end.x,cy:end.y,r:4,class:'route-endpoint'}));
|
||||
const vessel = svgEl('g',{transform:`translate(${point.x} ${point.y}) rotate(${angle})`});
|
||||
vessel.append(svgEl('circle',{cx:0,cy:0,r:6,class:'route-vessel-dot'}));
|
||||
vessel.append(svgEl('line',{x1:5,y1:0,x2:15,y2:0,class:'route-vessel-heading'}));
|
||||
group.append(vessel);
|
||||
const side = point.x > 760 ? -1 : 1;
|
||||
const labelX = point.x+side*24, labelY = point.y>340?point.y-20:point.y+27;
|
||||
const anchor = side<0?'end':'start';
|
||||
group.append(svgEl('line',{x1:point.x+side*8,y1:point.y-2,x2:labelX-side*4,y2:labelY-5,class:'route-label-leader'}));
|
||||
group.append(svgEl('text',{x:labelX,y:labelY,'text-anchor':anchor,class:'route-label-title'},asset.name));
|
||||
group.append(svgEl('text',{x:labelX,y:labelY+14,'text-anchor':anchor,class:'route-label-meta'},`${bodyNames[asset.transfer.from]||asset.transfer.from} → ${bodyNames[asset.transfer.to]||asset.transfer.to} · ${asset.transfer.progress}%`));
|
||||
}
|
||||
function drawAsset(x,y,label) {
|
||||
layers.asset.append(svgEl('circle',{cx:x,cy:y,r:5,class:'map-asset'}));
|
||||
layers.asset.append(svgEl('text',{x:x+10,y:y+4,class:'map-asset-label'},label));
|
||||
}
|
||||
function drawSolar() {
|
||||
planets.forEach(p=>orbit(500,310,p.orbit,p.orbit*.6,p.id==='earth'||p.id==='mars',p.kind));
|
||||
bodyButton('sun','Sun',500,310,34,true);
|
||||
planets.forEach(p=>{const point=planetPoint(p);bodyButton(p.id,systems[p.id].name,point.x,point.y,p.size,false,p.kind)});
|
||||
currentAssets().filter(a=>!a.transfer&&a.scope==='sun').slice(0,12).forEach((a,i)=>{const angle=.55+i*1.15;const radius=48+(i%4)*28;drawAsset(500+Math.cos(angle)*radius,310+Math.sin(angle)*radius*.6,a.name)});
|
||||
assets.filter(a=>a.transfer).forEach(drawTransfer);
|
||||
}
|
||||
function drawSystem(systemId) {
|
||||
const system = systems[systemId]; if (!system) return;
|
||||
bodyButton(systemId,system.name,500,310,48);
|
||||
const step = system.moons.length>5?42:system.moons.length>3?48:58;
|
||||
system.moons.forEach((moon,index)=>{const radius=105+index*step;orbit(500,310,radius,radius*.7,index===0);const angle=.7+index*1.45;bodyButton(moon,bodyNames[moon],500+Math.cos(angle)*radius,310+Math.sin(angle)*radius*.7,12+(index%3)*2)});
|
||||
assets.filter(a=>a.transfer).forEach(drawTransfer);
|
||||
}
|
||||
function drawLocal(bodyId) {
|
||||
bodyButton(bodyId,systems[bodyId]?.name||bodyNames[bodyId]||bodyId,500,310,68,bodyId==='sun');
|
||||
orbit(500,310,115,72,true); orbit(500,310,180,112); orbit(500,310,255,158);
|
||||
currentAssets().filter(a=>!a.transfer).slice(0,20).forEach((a,index)=>{const angle=.4+index*.78;const radius=115+(index%3)*65;drawAsset(500+Math.cos(angle)*radius,310+Math.sin(angle)*radius*.62,a.name)});
|
||||
}
|
||||
function openBody(id) {
|
||||
selectedRoute = null;
|
||||
if (view.level==='solar') view = id==='sun'?{level:'local',system:'sun',body:'sun'}:{level:'system',system:id,body:null};
|
||||
else if (view.level==='system') view = {level:'local',system:view.system,body:id};
|
||||
else if (view.system==='sun'&&id!=='sun') view = {level:'system',system:id,body:null};
|
||||
render();
|
||||
}
|
||||
function currentAssets() {
|
||||
if (view.level==='solar') return assets;
|
||||
if (view.system==='sun') return assets.filter(a=>a.scope==='sun');
|
||||
if (view.level==='system') return assets.filter(a=>a.scope===view.system);
|
||||
return assets.filter(a=>a.body===view.body);
|
||||
}
|
||||
function scopeName() {
|
||||
if (view.level==='solar') return 'Solar System';
|
||||
if (view.system==='sun') return 'Heliocentric Assets';
|
||||
if (view.level==='system') return systems[view.system].name+' System';
|
||||
return systems[view.body]?.name||bodyNames[view.body]||view.body;
|
||||
}
|
||||
function renderBreadcrumbs() {
|
||||
const wrap = root.querySelector('#location-breadcrumbs'); wrap.replaceChildren();
|
||||
const items = [{label:'Solar System',action:()=>{view={level:'solar',system:null,body:null};selectedRoute=null;render()}}];
|
||||
if (view.level!=='solar'&&view.system!=='sun') items.push({label:systems[view.system].name+' System',action:()=>{view={level:'system',system:view.system,body:null};selectedRoute=null;render()}});
|
||||
if (view.system==='sun') items.push({label:'Heliocentric Assets'}); else if (view.level==='local') items.push({label:scopeName()});
|
||||
items.forEach((item,index)=>{if(index){const sep=document.createElement('span');sep.className='sep';sep.textContent='›';wrap.append(sep)}const button=document.createElement('button');button.type='button';button.textContent=item.label;button.className=index===items.length-1?'current':'';if(item.action)button.addEventListener('click',item.action);wrap.append(button)});
|
||||
}
|
||||
function renderList() {
|
||||
const rows = currentAssets();
|
||||
root.querySelector('#scope-title').textContent=scopeName(); root.querySelector('#scope-count').textContent=rows.length+' assets'; root.querySelector('#panel-count').textContent=rows.length;
|
||||
const list=root.querySelector('#asset-list'); list.replaceChildren();
|
||||
if(!rows.length){const empty=document.createElement('div');empty.className='asset-empty';empty.textContent='当前范围内没有资产';list.append(empty);return}
|
||||
rows.forEach(asset=>{const button=document.createElement('button');button.type='button';button.className='asset-button '+(String(asset.state).toLowerCase().includes('planned')?'planned ':'')+(selectedRoute===asset.id?'selected':'');button.textContent=asset.name;button.addEventListener('click',()=>{selectedRoute=visibleTransfer(asset)?asset.id:null;render();openDrawer(asset)});list.append(button)});
|
||||
}
|
||||
function openDrawer(asset) {
|
||||
root.querySelector('#drawer-name').textContent=asset.name;
|
||||
const fields=root.querySelector('#drawer-fields');fields.replaceChildren();
|
||||
[['类型',asset.asset_type],['状态',asset.state],['位置',asset.location],['任务',asset.mission],['下一事件',asset.next_event]].forEach(([label,value])=>{const row=document.createElement('div');row.className='drawer-field';const key=document.createElement('span');key.textContent=label;const val=document.createElement('strong');val.textContent=value||'—';row.append(key,val);fields.append(row)});
|
||||
root.querySelector('#drawer-link').href=asset.detail_url;
|
||||
root.querySelector('#asset-drawer').classList.add('open'); root.querySelector('#asset-drawer').setAttribute('aria-hidden','false');
|
||||
}
|
||||
function closeDrawer(){root.querySelector('#asset-drawer').classList.remove('open');root.querySelector('#asset-drawer').setAttribute('aria-hidden','true')}
|
||||
function render() {
|
||||
closeDrawer(); clearMap();
|
||||
if(view.level==='solar'||view.system==='sun')drawSolar();else if(view.level==='system')drawSystem(view.system);else drawLocal(view.body);
|
||||
const hasRoutes=currentAssets().some(visibleTransfer);
|
||||
const selected=assets.find(a=>a.id===selectedRoute);
|
||||
root.querySelector('#view-mode').textContent=view.level==='solar'?'SOLAR SYSTEM':view.system==='sun'?'HELIOCENTRIC':view.level==='system'?'PLANETARY SYSTEM':'LOCAL ORBITS';
|
||||
root.querySelector('#map-caption').textContent=selected?'已选择转移轨迹 · '+selected.name:showAllRoutes&&hasRoutes?'正在显示全部转移轨迹':view.level==='system'?'选择行星、卫星或转移资产':'选择天体或转移资产';
|
||||
const toggle=root.querySelector('#transfer-toggle');toggle.checked=showAllRoutes&&hasRoutes;toggle.disabled=!hasRoutes;
|
||||
renderBreadcrumbs();renderList();
|
||||
}
|
||||
root.querySelector('#drawer-close').addEventListener('click',closeDrawer);
|
||||
root.querySelector('#transfer-toggle').addEventListener('change',event=>{showAllRoutes=event.target.checked;render()});
|
||||
root.querySelector('#location-reset').addEventListener('click',()=>{view={level:'solar',system:null,body:null};selectedRoute=null;showAllRoutes=false;render()});
|
||||
let resizeFrame=0;window.addEventListener('resize',()=>{cancelAnimationFrame(resizeFrame);resizeFrame=requestAnimationFrame(render)});
|
||||
render();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -194,7 +194,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script id="tl-json" type="application/json">{{ timeline_json|safe }}</script>
|
||||
<script id="tl-json" type="application/json">{{ timeline_data|tojson }}</script>
|
||||
<script>
|
||||
(function(){
|
||||
const viewport = document.getElementById('tl-viewport');
|
||||
|
||||
Reference in New Issue
Block a user