2026-05-21 15:24:56 +08:00

99 lines
6.8 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>光湖服务器主控台</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{--bg:#080c18;--s1:#0f1623;--s2:#141e2e;--s3:#1a2640;--border:#1e3251;--accent:#4f8ef7;--text:#e2e8f0;--dim:#64748b;--ok:#34d399;--warn:#fbbf24;--err:#f87171;--r:14px;--rs:8px}
body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;font-size:14px;padding:24px;max-width:1200px;margin:0 auto}
h1{font-size:22px;margin-bottom:4px;display:flex;align-items:center;gap:10px}
.sub{color:var(--dim);font-size:13px;margin-bottom:24px}
.dashboard{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;margin-bottom:24px}
.card{background:var(--s1);border:1px solid var(--border);border-radius:var(--r);padding:16px;transition:border-color .3s}
.card.alive{border-color:rgba(52,211,153,.3)}
.card.dead{border-color:rgba(248,113,113,.3)}
.card-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
.card-name{font-size:14px;font-weight:500}
.dot{width:10px;height:10px;border-radius:50%;display:inline-block}
.dot.alive{background:var(--ok);box-shadow:0 0 8px rgba(52,211,153,.5)}
.dot.dead{background:var(--err);box-shadow:0 0 8px rgba(248,113,113,.5)}
.card-meta{display:flex;gap:12px;font-size:12px;color:var(--dim);margin-bottom:8px;flex-wrap:wrap}
.card-meta span{background:var(--s2);padding:2px 8px;border-radius:4px}
.card-status{font-size:12px}
.card-status .ok{color:var(--ok)}
.card-status .err{color:var(--err)}
.last-seen{font-size:11px;color:var(--dim);margin-top:8px}
.panel{background:var(--s1);border:1px solid var(--border);border-radius:var(--r);padding:20px;margin-bottom:24px}
.panel h2{font-size:16px;margin-bottom:12px;color:var(--accent)}
.request-box{background:var(--s2);border:1px solid var(--border);border-radius:var(--rs);padding:12px 16px;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center}
.request-box .info{flex:1}
.request-box .target{font-size:14px;font-weight:500}
.request-box .reason{font-size:12px;color:var(--dim)}
.btn{padding:8px 16px;border:none;border-radius:var(--rs);font-size:13px;cursor:pointer;font-family:inherit}
.btn-ok{background:var(--ok);color:#000}
.btn-ok:hover{opacity:.85}
.btn-no{background:var(--s3);color:var(--text);border:1px solid var(--border);margin-left:8px}
.btn-no:hover{border-color:var(--err)}
.empty{color:var(--dim);font-size:13px;text-align:center;padding:20px}
.refresh-btn{background:var(--s3);border:1px solid var(--border);color:var(--text);padding:6px 14px;border-radius:var(--rs);cursor:pointer;font-size:12px;font-family:inherit}
.refresh-btn:hover{border-color:var(--accent)}
.header-bar{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}
.header-right{display:flex;gap:10px;align-items:center}
.toast{position:fixed;bottom:24px;right:24px;background:var(--s3);border:1px solid var(--ok);border-radius:var(--rs);padding:12px 20px;font-size:13px;color:var(--ok);display:none;z-index:100;max-width:400px}
.breathing{animation:breathe 2s ease-in-out infinite}
@keyframes breathe{0%,100%{opacity:1}50%{opacity:.4}}
</style>
</head>
<body>
<div class="header-bar">
<div>
<h1><span>光湖服务器主控台</span></h1>
<div class="sub">铸渊看门人 · 六台服务器实时状态</div>
</div>
<div class="header-right">
<span id="clock" style="font-size:12px;color:var(--dim)"></span>
<button class="refresh-btn" onclick="refreshAll()">刷新</button>
</div>
</div>
<div class="dashboard" id="dashboard"></div>
<div class="panel">
<h2 id="pending-title">铸渊操作请求</h2>
<div id="pending-requests"><div class="empty">暂无待处理的请求</div></div>
</div>
<div id="toast" class="toast"></div>
<script>
const API='/console/api';let iv;
function toast(m,c){const t=document.getElementById('toast');t.textContent=m;t.style.borderColor=c||'var(--ok)';t.style.color=c||'var(--ok)';t.style.display='block';setTimeout(()=>t.style.display='none',3000)}
async function fetchS(){try{const r=await fetch(API+'/servers');return await r.json()}catch(e){return{servers:[]}}}
async function fetchP(){try{const r=await fetch(API+'/pending');return await r.json()}catch(e){return{requests:[]}}}
function renderS(data){
const d=document.getElementById('dashboard'),sv=data.servers||[];
if(!sv.length){d.innerHTML='<div class="empty">正在采集...</div>';return}
const alive=sv.filter(s=>s.alive).length;
document.querySelector('.sub').textContent='铸渊看门人 · '+alive+'/'+sv.length+' 台在线';
d.innerHTML=sv.map(s=>{
const c=s.alive?'alive':'dead',st=s.alive?'在线':'离线',lt=s.alive?' &middot; '+s.latency+'ms':'';
const ls=s.last_seen?new Date(s.last_seen).toLocaleTimeString('zh-CN',{hour12:false}):'-';
return '<div class="card '+c+'"><div class="card-head"><span class="card-name">'+s.name+'</span><span class="dot '+c+(s.alive?' breathing':'')+'"></span></div><div class="card-meta"><span>'+s.code+'</span><span>'+s.ip+'</span>'+(s.latency?'<span>'+s.latency+'ms</span>':'')+'</div><div class="card-status"><span class="'+(s.alive?'ok':'err')+'">'+st+lt+'</span></div><div class="last-seen">最后: '+ls+'</div></div>'
}).join('')
}
function renderP(data){
const d=document.getElementById('pending-requests'),req=data.requests||[],t=document.getElementById('pending-title');
if(!req.length){t.textContent='暂无操作请求';d.innerHTML='<div class="empty">铸渊没有待处理的请求</div>';return}
t.textContent=req.length+' 条待处理';
d.innerHTML=req.map(r=>'<div class="request-box"><div class="info"><div class="target">'+r.target_name+'</div><div class="reason">'+r.reason+'</div><div style="font-size:11px;color:var(--dim);margin-top:4px">'+new Date(r.created_at).toLocaleTimeString('zh-CN',{hour12:false})+'</div></div><div><button class="btn btn-ok" onclick="auth(\''+r.id+'\')">授权</button><button class="btn btn-no" onclick="rej(\''+r.id+'\')">拒绝</button></div></div>').join('')
}
async function auth(id){
try{const r=await fetch(API+'/authorize',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({request_id:id})});const d=await r.json();if(d.ok){toast('已授权');refreshAll()}}catch(e){toast('失败: '+e.message,'var(--err)')}
}
async function rej(id){
try{await fetch(API+'/reject',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({request_id:id})});toast('已拒绝');refreshAll()}catch(e){}
}
async function refreshAll(){const[s,p]=await Promise.all([fetchS(),fetchP()]);renderS(s);renderP(p)}
function clock(){document.getElementById('clock').textContent=new Date().toLocaleString('zh-CN',{hour12:false})}
refreshAll();clock();iv=setInterval(refreshAll,10000);setInterval(clock,1000);
</script>
</body>
</html>