301 lines
11 KiB
JavaScript
301 lines
11 KiB
JavaScript
|
|
/* planet.js · 湖面映星 · 星球绘制与交互(独立文件,避免内联脚本被宿主吞掉) */
|
|||
|
|
window.__boot = "yes";
|
|||
|
|
window.onerror = function(msg, src, ln, col){
|
|||
|
|
try { if (window.pywebview && window.pywebview.api)
|
|||
|
|
window.pywebview.api.log("ERR " + msg + " @" + ln); } catch(e){}
|
|||
|
|
};
|
|||
|
|
var SATS = [
|
|||
|
|
{key:"Qwen3.8-Max", name:"Qwen 3.8 Max"},
|
|||
|
|
{key:"Qwen3.7-Max", name:"Qwen 3.7 Max"},
|
|||
|
|
{key:"DeepSeek-v4-Pro", name:"DeepSeek v4 Pro"},
|
|||
|
|
{key:"custom", name:"自定义合计"}
|
|||
|
|
];
|
|||
|
|
var $ = function(s){ return document.querySelector(s); };
|
|||
|
|
function fmt(n){
|
|||
|
|
return n>=1e6 ? (n/1e6).toFixed(2)+"M"
|
|||
|
|
: n>=1e3 ? (n/1e3).toFixed(1)+"k" : String(Math.round(n));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var S = 500, C = S/2, PR = 106;
|
|||
|
|
var TILT = -0.42, RX = 190, RY = 54;
|
|||
|
|
var RR = PR+12;
|
|||
|
|
var SYSR = [PR+30, PR+42, PR+54];
|
|||
|
|
var cv = $("#cv"), ctx = cv.getContext("2d");
|
|||
|
|
var dpr = window.devicePixelRatio || 1;
|
|||
|
|
cv.width = S*dpr; cv.height = S*dpr;
|
|||
|
|
cv.style.width = cv.style.height = S+"px";
|
|||
|
|
ctx.scale(dpr,dpr);
|
|||
|
|
|
|||
|
|
var data = {usage:{}, plan:{}, sys:{}};
|
|||
|
|
var core = {cap:"今 日 投 影", big:"--", sub:"token"};
|
|||
|
|
var ringFrac = null, ringShown = 0;
|
|||
|
|
var sysShown = [0,0,0];
|
|||
|
|
var satPos = [], sysHit = [];
|
|||
|
|
|
|||
|
|
function tokSince(plan, usage){
|
|||
|
|
var now = usage.tokens||{}, base = plan.base_tokens||{};
|
|||
|
|
var s = 0, k;
|
|||
|
|
for (k in now) s += Math.max(0, now[k]-(base[k]||0));
|
|||
|
|
return s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function compute(d){
|
|||
|
|
data = d;
|
|||
|
|
var plan = d.plan||{}, usage = d.usage||{};
|
|||
|
|
if (plan.capacity && plan.remain != null){
|
|||
|
|
var spent = Math.max(0, plan.capacity-plan.remain) + tokSince(plan, usage);
|
|||
|
|
var left = Math.max(0, plan.capacity-spent);
|
|||
|
|
ringFrac = left/plan.capacity;
|
|||
|
|
core = {cap:"余 量", big:fmt(left), sub:"共 "+fmt(plan.capacity)};
|
|||
|
|
} else {
|
|||
|
|
ringFrac = null;
|
|||
|
|
var tot = 0, t = usage.tokens||{}, k2;
|
|||
|
|
for (k2 in t) tot += t[k2];
|
|||
|
|
core = {cap:"今 日 投 影", big:fmt(tot), sub:"token · 本地估算"};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var bgStars = [];
|
|||
|
|
for (var bi=0; bi<26; bi++){
|
|||
|
|
bgStars.push({x:Math.random()*S, y:Math.random()*S,
|
|||
|
|
r:Math.random()*1.1+.3, p:Math.random()*7,
|
|||
|
|
s:.005+Math.random()*.011, warm:Math.random()<.6});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function orbitXY(a){
|
|||
|
|
return [C + RX*Math.cos(a)*Math.cos(TILT) - RY*Math.sin(a)*Math.sin(TILT),
|
|||
|
|
C + RY*Math.sin(a)*Math.cos(TILT) + RX*Math.cos(a)*Math.sin(TILT)];
|
|||
|
|
}
|
|||
|
|
function drawOrbit(front){
|
|||
|
|
ctx.beginPath();
|
|||
|
|
ctx.ellipse(C,C,RX,RY,TILT, front?0:Math.PI, front?Math.PI:Math.PI*2);
|
|||
|
|
ctx.strokeStyle = front ? "rgba(255,233,189,.26)" : "rgba(143,154,196,.14)";
|
|||
|
|
ctx.lineWidth = 1; ctx.stroke();
|
|||
|
|
}
|
|||
|
|
function drawSats(t, front){
|
|||
|
|
SATS.forEach(function(m,i){
|
|||
|
|
var a = t*.00022*(1+i*.13) + i*Math.PI/2;
|
|||
|
|
if ((Math.sin(a)>0) !== front) return;
|
|||
|
|
var xy = orbitXY(a), x=xy[0], y=xy[1];
|
|||
|
|
if (front) satPos[i] = {x:x, y:y};
|
|||
|
|
var req = (data.usage.requests||{})[m.key]||0;
|
|||
|
|
var glow = req>0 ? 1 : .45;
|
|||
|
|
ctx.beginPath(); ctx.arc(x,y,3.4,0,7);
|
|||
|
|
ctx.fillStyle = "rgba(255,233,189,"+(.95*glow)+")";
|
|||
|
|
ctx.shadowColor = "rgba(255,233,189,.95)"; ctx.shadowBlur = 12*glow;
|
|||
|
|
ctx.fill(); ctx.shadowBlur = 0;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function sysVals(){
|
|||
|
|
var s = data.sys||{};
|
|||
|
|
return [
|
|||
|
|
{label:"内存", pct:(s.mem||{}).used_pct||0,
|
|||
|
|
detail:function(){var m=data.sys.mem||{};
|
|||
|
|
return "内存 "+(m.used_gb||0)+" / "+(m.total_gb||0)+" GB";}},
|
|||
|
|
{label:"存储", pct:(s.disk||{}).used_pct||0,
|
|||
|
|
detail:function(){var d=data.sys.disk||{};
|
|||
|
|
return "存储 余 "+(d.free_gb||0)+" / "+(d.total_gb||0)+" GB";}},
|
|||
|
|
{label:"处理", pct:(s.cpu||{}).pct||0,
|
|||
|
|
detail:function(){return "处理器 "+((data.sys.cpu||{}).pct||0)+"%";}}
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function drawSysRings(t, breathe){
|
|||
|
|
var vs = sysVals();
|
|||
|
|
sysHit = [];
|
|||
|
|
vs.forEach(function(v,i){
|
|||
|
|
var r = SYSR[i];
|
|||
|
|
sysShown[i] += ((v.pct/100) - sysShown[i])*.08;
|
|||
|
|
var a0 = -Math.PI/2;
|
|||
|
|
ctx.beginPath(); ctx.arc(C,C,r,0,7);
|
|||
|
|
ctx.strokeStyle="rgba(143,154,196,.10)"; ctx.lineWidth=1.4; ctx.stroke();
|
|||
|
|
var frac = Math.max(.005, sysShown[i]);
|
|||
|
|
ctx.beginPath(); ctx.arc(C,C,r,a0,a0+Math.PI*2*frac);
|
|||
|
|
var hot = v.pct>85;
|
|||
|
|
ctx.strokeStyle = hot ? "rgba(255,180,150,"+(.85*breathe)+")"
|
|||
|
|
: "rgba(196,210,255,"+(.55*breathe)+")";
|
|||
|
|
ctx.lineWidth=1.4; ctx.stroke();
|
|||
|
|
var hx=C+r*Math.cos(a0+Math.PI*2*frac), hy=C+r*Math.sin(a0+Math.PI*2*frac);
|
|||
|
|
ctx.beginPath(); ctx.arc(hx,hy,2,0,7);
|
|||
|
|
ctx.fillStyle = hot ? "rgba(255,180,150,.95)" : "rgba(220,228,255,.9)";
|
|||
|
|
ctx.fill();
|
|||
|
|
ctx.textAlign="right";
|
|||
|
|
ctx.fillStyle="rgba(122,133,176,"+(.9*breathe)+")";
|
|||
|
|
ctx.font="300 9px 'PingFang SC'";
|
|||
|
|
ctx.fillText(v.label+" "+Math.round(v.pct), C-r-9, C+3);
|
|||
|
|
sysHit.push({r:r, pct:v.pct, label:v.label, detail:v.detail});
|
|||
|
|
});
|
|||
|
|
ctx.textAlign="center";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function draw(){
|
|||
|
|
try { drawInner(performance.now()); } catch(e){
|
|||
|
|
try { window.pywebview && window.pywebview.api &&
|
|||
|
|
window.pywebview.api.log("DRAW ERR "+e); } catch(e2){}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 不依赖 requestAnimationFrame:失焦窗口里 rAF 会被宿主暂停,改用定时器保活
|
|||
|
|
setInterval(draw, 66);
|
|||
|
|
|
|||
|
|
function drawInner(t){
|
|||
|
|
ctx.clearRect(0,0,S,S);
|
|||
|
|
|
|||
|
|
bgStars.forEach(function(st){
|
|||
|
|
st.p += st.s;
|
|||
|
|
var a = .15 + .6*Math.abs(Math.sin(st.p));
|
|||
|
|
ctx.beginPath(); ctx.arc(st.x,st.y,st.r,0,7);
|
|||
|
|
ctx.fillStyle = st.warm ? "rgba(255,233,189,"+a+")"
|
|||
|
|
: "rgba(196,210,255,"+(a*.7)+")";
|
|||
|
|
ctx.fill();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
var breathe = .82 + .18*Math.sin(t/1400);
|
|||
|
|
|
|||
|
|
var g = ctx.createRadialGradient(C,C,PR*.6,C,C,PR*2);
|
|||
|
|
g.addColorStop(0, "rgba(96,110,200,"+(.18*breathe)+")");
|
|||
|
|
g.addColorStop(.6, "rgba(70,80,160,"+(.08*breathe)+")");
|
|||
|
|
g.addColorStop(1, "rgba(0,0,0,0)");
|
|||
|
|
ctx.fillStyle=g; ctx.fillRect(0,0,S,S);
|
|||
|
|
|
|||
|
|
drawOrbit(false); drawSats(t,false);
|
|||
|
|
|
|||
|
|
g = ctx.createRadialGradient(C-PR*.38, C-PR*.42, PR*.1, C, C, PR*1.05);
|
|||
|
|
g.addColorStop(0, "#33437f"); g.addColorStop(.45, "#18224e");
|
|||
|
|
g.addColorStop(.8, "#0a1028"); g.addColorStop(1, "#05070f");
|
|||
|
|
ctx.beginPath(); ctx.arc(C,C,PR,0,7); ctx.fillStyle=g; ctx.fill();
|
|||
|
|
|
|||
|
|
ctx.save();
|
|||
|
|
ctx.beginPath(); ctx.arc(C,C,PR,0,7); ctx.clip();
|
|||
|
|
for (var i=0;i<2;i++){
|
|||
|
|
var off = ((t/9000 + i*.5)%1)*PR*2 - PR;
|
|||
|
|
ctx.beginPath();
|
|||
|
|
ctx.ellipse(C, C+off*.7, PR*1.1, PR*.16, -.2+i*.35, 0, 7);
|
|||
|
|
ctx.fillStyle = "rgba(120,140,220,"+(.05+.03*Math.sin(t/2000+i))+")";
|
|||
|
|
ctx.fill();
|
|||
|
|
}
|
|||
|
|
g = ctx.createRadialGradient(C+PR*.55, C+PR*.55, PR*.1, C+PR*.55, C+PR*.55, PR*1.15);
|
|||
|
|
g.addColorStop(0,"rgba(255,233,189,0)"); g.addColorStop(.72,"rgba(255,233,189,0)");
|
|||
|
|
g.addColorStop(.92,"rgba(255,233,189,"+(.30*breathe)+")"); g.addColorStop(1,"rgba(255,233,189,0)");
|
|||
|
|
ctx.fillStyle=g; ctx.fillRect(0,0,S,S);
|
|||
|
|
ctx.restore();
|
|||
|
|
|
|||
|
|
ctx.beginPath(); ctx.arc(C,C,PR,0,7);
|
|||
|
|
ctx.strokeStyle="rgba(196,210,255,"+(.16*breathe)+")"; ctx.lineWidth=1; ctx.stroke();
|
|||
|
|
|
|||
|
|
ctx.textAlign="center";
|
|||
|
|
ctx.fillStyle="rgba(90,100,140,.95)"; ctx.font="400 10px 'PingFang SC'";
|
|||
|
|
ctx.fillText(core.cap, C, C-30);
|
|||
|
|
ctx.fillStyle="#ffe9bd";
|
|||
|
|
ctx.shadowColor="rgba(255,233,189,.55)"; ctx.shadowBlur=18;
|
|||
|
|
ctx.font="200 38px 'PingFang SC'";
|
|||
|
|
ctx.fillText(core.big, C, C+10);
|
|||
|
|
ctx.shadowBlur=0;
|
|||
|
|
ctx.fillStyle="rgba(143,154,196,.85)"; ctx.font="300 10px 'PingFang SC'";
|
|||
|
|
ctx.fillText(core.sub, C, C+32);
|
|||
|
|
|
|||
|
|
var target = ringFrac==null ? 1 : ringFrac;
|
|||
|
|
ringShown += (target-ringShown)*.06;
|
|||
|
|
var a0=-Math.PI/2;
|
|||
|
|
ctx.beginPath(); ctx.arc(C,C,RR,0,7);
|
|||
|
|
ctx.strokeStyle="rgba(143,154,196,.13)"; ctx.lineWidth=2; ctx.stroke();
|
|||
|
|
ctx.beginPath(); ctx.arc(C,C,RR,a0,a0+Math.PI*2*ringShown);
|
|||
|
|
ctx.strokeStyle="rgba(255,233,189,"+(.8*breathe)+")"; ctx.lineWidth=2;
|
|||
|
|
ctx.shadowColor="rgba(255,233,189,.8)"; ctx.shadowBlur=8; ctx.stroke();
|
|||
|
|
ctx.shadowBlur=0;
|
|||
|
|
var hx=C+RR*Math.cos(a0+Math.PI*2*ringShown),
|
|||
|
|
hy=C+RR*Math.sin(a0+Math.PI*2*ringShown);
|
|||
|
|
ctx.beginPath(); ctx.arc(hx,hy,3*(.7+.3*Math.sin(t/420)),0,7);
|
|||
|
|
ctx.fillStyle="#ffe9bd"; ctx.shadowColor="rgba(255,233,189,1)";
|
|||
|
|
ctx.shadowBlur=10; ctx.fill(); ctx.shadowBlur=0;
|
|||
|
|
|
|||
|
|
drawSysRings(t, breathe);
|
|||
|
|
|
|||
|
|
drawOrbit(true); drawSats(t,true);
|
|||
|
|
|
|||
|
|
ctx.fillStyle="rgba(90,100,140,.5)";
|
|||
|
|
ctx.font="300 9px 'PingFang SC'";
|
|||
|
|
ctx.fillText("拖 动 移 星 · 轻 点 校 准", C, S-12);
|
|||
|
|
}
|
|||
|
|
draw();
|
|||
|
|
|
|||
|
|
/* ---------- 交互 ---------- */
|
|||
|
|
var down=false, moved=false, lx=0, ly=0;
|
|||
|
|
cv.addEventListener("mousedown", function(e){
|
|||
|
|
down=true; moved=false; lx=e.screenX; ly=e.screenY;
|
|||
|
|
});
|
|||
|
|
window.addEventListener("mousemove", function(e){
|
|||
|
|
if (down){
|
|||
|
|
var dx=e.screenX-lx, dy=e.screenY-ly;
|
|||
|
|
if (Math.abs(dx)+Math.abs(dy)>3){ moved=true; cv.classList.add("dragging"); }
|
|||
|
|
if (moved) window.pywebview.api.drag_by(dx,dy);
|
|||
|
|
lx=e.screenX; ly=e.screenY; hideTip();
|
|||
|
|
} else { hover(e); }
|
|||
|
|
});
|
|||
|
|
window.addEventListener("mouseup", function(e){
|
|||
|
|
if (down && !moved){
|
|||
|
|
var dx=e.offsetX-C, dy=e.offsetY-C;
|
|||
|
|
if (dx*dx+dy*dy <= PR*PR) openPanel();
|
|||
|
|
}
|
|||
|
|
down=false; cv.classList.remove("dragging");
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
function hover(e){
|
|||
|
|
var i, p, dx, dy;
|
|||
|
|
for (i=0;i<SATS.length;i++){
|
|||
|
|
p=satPos[i]; if(!p) continue;
|
|||
|
|
dx=e.offsetX-p.x; dy=e.offsetY-p.y;
|
|||
|
|
if (dx*dx+dy*dy < 170){
|
|||
|
|
var m=SATS[i];
|
|||
|
|
var tok=(data.usage.tokens||{})[m.key]||0;
|
|||
|
|
var req=(data.usage.requests||{})[m.key]||0;
|
|||
|
|
showTip(p.x,p.y,m.name+" · "+fmt(tok)+" · "+req+" 次");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
var cx=e.offsetX-C, cy=e.offsetY-C, dist=Math.sqrt(cx*cx+cy*cy);
|
|||
|
|
for (i=0;i<sysHit.length;i++){
|
|||
|
|
var h=sysHit[i];
|
|||
|
|
if (Math.abs(dist-h.r) < 4.5){
|
|||
|
|
var a = Math.atan2(cy,cx);
|
|||
|
|
showTip(C+h.r*Math.cos(a), C+h.r*Math.sin(a), h.detail());
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
hideTip();
|
|||
|
|
}
|
|||
|
|
function showTip(x,y,txt){
|
|||
|
|
var tip=$("#tip");
|
|||
|
|
tip.textContent=txt; tip.style.left=x+"px"; tip.style.top=y+"px";
|
|||
|
|
tip.style.display="block";
|
|||
|
|
}
|
|||
|
|
function hideTip(){ $("#tip").style.display="none"; }
|
|||
|
|
|
|||
|
|
/* ---------- 数据与校准 ---------- */
|
|||
|
|
function refresh(){
|
|||
|
|
window.pywebview.api.get_data().then(function(d){ compute(d); },
|
|||
|
|
function(){});
|
|||
|
|
}
|
|||
|
|
function openPanel(){
|
|||
|
|
$("#panel").classList.add("open");
|
|||
|
|
window.pywebview.api.get_data().then(function(d){
|
|||
|
|
var p=d.plan||{};
|
|||
|
|
$("#p_cap").value=p.capacity||"";
|
|||
|
|
$("#p_rem").value=(p.remain==null ? "" : p.remain);
|
|||
|
|
}, function(){});
|
|||
|
|
}
|
|||
|
|
function closePanel(){ $("#panel").classList.remove("open"); }
|
|||
|
|
function savePlan(){
|
|||
|
|
var at=new Date();
|
|||
|
|
function pad(n){ return String(n).padStart(2,"0"); }
|
|||
|
|
var stamp=at.getFullYear()+"-"+pad(at.getMonth()+1)+"-"+pad(at.getDate())
|
|||
|
|
+" "+pad(at.getHours())+":"+pad(at.getMinutes());
|
|||
|
|
window.pywebview.api.get_data().then(function(d){
|
|||
|
|
var plan={ capacity:+($("#p_cap").value)||0,
|
|||
|
|
remain: $("#p_rem").value===""?null:+($("#p_rem").value),
|
|||
|
|
set_at:stamp, set_day:stamp.slice(0,10),
|
|||
|
|
base_tokens:d.usage.tokens||{} };
|
|||
|
|
return window.pywebview.api.set_plan(JSON.stringify(plan));
|
|||
|
|
}).then(function(){ closePanel(); refresh(); }, function(){});
|
|||
|
|
}
|
|||
|
|
refresh(); setInterval(refresh, 2000);
|