mirror of
https://github.com//cppla/ServerStatus
synced 2025-12-15 10:12:04 +08:00
update theme
This commit is contained in:
374
web/js/app.js
Normal file
374
web/js/app.js
Normal file
@@ -0,0 +1,374 @@
|
||||
// 简洁现代前端 - 仅使用原生 JS
|
||||
const S = { updated:0, servers:[], ssl:[], error:false, hist:{}, metricHist:{}, loadHist:{} };// hist latency; metricHist: {key:{cpu:[],mem:[],hdd:[]}}; loadHist: {key:[]}
|
||||
const els = {
|
||||
notice: ()=>document.getElementById('notice'),
|
||||
last: ()=>document.getElementById('lastUpdate'),
|
||||
serversBody: ()=>document.getElementById('serversBody'),
|
||||
monitorsBody: ()=>document.getElementById('monitorsBody'),
|
||||
sslBody: ()=>document.getElementById('sslBody')
|
||||
};
|
||||
|
||||
function bytes(v){ if(v===0) return '0B'; if(!v) return '-'; const k=1000; const u=['B','KB','MB','GB','TB','PB']; const i=Math.floor(Math.log(v)/Math.log(k)); return (v/Math.pow(k,i)).toFixed(i?1:0)+u[i]; }
|
||||
function pct(v){ return (v||0).toFixed(0)+'%'; }
|
||||
function clsBy(v){ return v>=90?'danger':v>=80?'warn':'ok'; }
|
||||
function humanAgo(ts){ if(!ts) return '-'; const s=Math.floor((Date.now()/1000 - ts)); const m=Math.floor(s/60); return m>0? m+' 分钟前':'几秒前'; }
|
||||
function num(v){ return (typeof v==='number' && !isNaN(v)) ? v : '-'; }
|
||||
|
||||
async function fetchData(){
|
||||
try {
|
||||
const r = await fetch('json/stats.json?_='+Date.now());
|
||||
if(!r.ok) throw new Error(r.status);
|
||||
const j = await r.json();
|
||||
if(j.reload) location.reload();
|
||||
S.updated = j.updated; S.servers = j.servers||[]; S.ssl = j.sslcerts||[]; S.error=false;
|
||||
// 更新延迟历史 (按节点名聚合)
|
||||
S.servers.forEach(s=>{
|
||||
const key = s.name || s.location || 'node';
|
||||
if(!S.hist[key]) S.hist[key] = {cu:[],ct:[],cm:[]};
|
||||
const H = S.hist[key];
|
||||
// 使用 time_ 字段 (ms) 若不存在则跳过
|
||||
if(typeof s.time_10010 === 'number') H.cu.push(s.time_10010);
|
||||
if(typeof s.time_189 === 'number') H.ct.push(s.time_189);
|
||||
if(typeof s.time_10086 === 'number') H.cm.push(s.time_10086);
|
||||
const MAX=120; // 保留约 120*4s ≈ 8 分钟
|
||||
['cu','ct','cm'].forEach(k=>{ if(H[k].length>MAX) H[k].splice(0,H[k].length-MAX); });
|
||||
// 指标历史 (仅在线时记录)
|
||||
if(!S.metricHist[key]) S.metricHist[key] = {cpu:[],mem:[],hdd:[]};
|
||||
const MH = S.metricHist[key];
|
||||
if(s.online4||s.online6){
|
||||
const memPct = s.memory_total? (s.memory_used/s.memory_total*100):0;
|
||||
const hddPct = s.hdd_total? (s.hdd_used/s.hdd_total*100):0;
|
||||
MH.cpu.push(s.cpu||0);
|
||||
MH.mem.push(memPct||0);
|
||||
MH.hdd.push(hddPct||0);
|
||||
const MAXM=120; ['cpu','mem','hdd'].forEach(k=>{ if(MH[k].length>MAXM) MH[k].splice(0,MH[k].length-MAXM); });
|
||||
}
|
||||
// 负载历史 (记录 load_1 / load_5 / load_15)
|
||||
if(!S.loadHist[key]) S.loadHist[key] = {l1:[],l5:[],l15:[]};
|
||||
const LH = S.loadHist[key];
|
||||
const pushLoad = (arr,val)=>{ if(typeof val === 'number' && val >= 0){ arr.push(val); if(arr.length>120) arr.splice(0,arr.length-120); } };
|
||||
pushLoad(LH.l1, s.load_1);
|
||||
pushLoad(LH.l5, s.load_5);
|
||||
pushLoad(LH.l15, s.load_15);
|
||||
});
|
||||
render();
|
||||
}catch(e){ S.error=true; els.notice().textContent = '数据获取失败'; console.error(e); }
|
||||
}
|
||||
|
||||
function render(){
|
||||
els.notice().style.display='none';
|
||||
renderServers();
|
||||
renderServersCards();
|
||||
renderMonitors();
|
||||
renderSSL();
|
||||
updateTime();
|
||||
}
|
||||
function renderServers(){
|
||||
const tbody = els.serversBody();
|
||||
let html='';
|
||||
S.servers.forEach((s,idx)=>{
|
||||
const online = s.online4||s.online6;
|
||||
const proto = online ? (s.online4 && s.online6? '双栈': s.online4? 'IPv4':'IPv6') : '离线';
|
||||
const statusPill = online ? `<span class="pill on">${proto}</span>` : `<span class="pill off">${proto}</span>`;
|
||||
const cpuCls = clsBy(s.cpu);
|
||||
const memPct = s.memory_total? (s.memory_used/s.memory_total*100):0; const memCls = clsBy(memPct);
|
||||
const hddPct = s.hdd_total? (s.hdd_used/s.hdd_total*100):0; const hddCls = clsBy(hddPct);
|
||||
const monthIn = bytes(s.network_in - s.last_network_in);
|
||||
const monthOut = bytes(s.network_out - s.last_network_out);
|
||||
const netNow = bytes(s.network_rx)+' | '+bytes(s.network_tx);
|
||||
const netTotal = bytes(s.network_in)+' | '+bytes(s.network_out);
|
||||
const p1 = (s.ping_10010||0); const p2 = (s.ping_189||0); const p3 = (s.ping_10086||0);
|
||||
function bucket(p){ const v = Math.max(0, Math.min(100, p)); const level = v>=20?'bad':(v>=10?'warn':'ok'); return `<div class=\"bucket\" data-lv=\"${level}\"><span style=\"--h:${v}%\"></span><label>${v.toFixed(0)}%</label></div>`; }
|
||||
const pingBuckets = `<div class=\"buckets\" title=\"CU/CT/CM\">${bucket(p1)}${bucket(p2)}${bucket(p3)}</div>`;
|
||||
const key = s.name || s.location || 'node';
|
||||
html += `<tr data-idx="${idx}" class="row-server" style="cursor:pointer;">
|
||||
<td>${statusPill}</td>
|
||||
<td>${s.name||'-'}</td>
|
||||
<td>${s.type||'-'}</td>
|
||||
<td>${s.location||'-'}</td>
|
||||
<td>${s.uptime||'-'}</td>
|
||||
<td>${s.load_1==-1?'–':s.load_1?.toFixed(2)}</td>
|
||||
<td>${monthIn} | ${monthOut}</td>
|
||||
<td>${netNow}</td>
|
||||
<td>${netTotal}</td>
|
||||
<td>${online?`<div class="spark" data-key="${key}" data-metric="cpu" title="CPU ${s.cpu||0}%"></div>`:'-'}</td>
|
||||
<td>${online?`<div class="spark" data-key="${key}" data-metric="mem" title="内存 ${memPct.toFixed(0)}%"></div>`:'-'}</td>
|
||||
<td>${online?`<div class="spark" data-key="${key}" data-metric="hdd" title="硬盘 ${hddPct.toFixed(0)}%"></div>`:'-'}</td>
|
||||
<td>${pingBuckets}</td>
|
||||
</tr>`;
|
||||
});
|
||||
tbody.innerHTML = html || `<tr><td colspan="13" class="muted" style="text-align:center;padding:1rem;">无数据</td></tr>`;
|
||||
|
||||
// 绑定行点击
|
||||
tbody.querySelectorAll('tr.row-server').forEach(tr=>{
|
||||
tr.addEventListener('click',()=>{
|
||||
const i = parseInt(tr.getAttribute('data-idx'));
|
||||
openDetail(i);
|
||||
});
|
||||
});
|
||||
|
||||
drawSparks();
|
||||
}
|
||||
function renderServersCards(){
|
||||
const wrap = document.getElementById('serversCards');
|
||||
if(!wrap) return;
|
||||
// 仅在窄屏时显示 (和 CSS 一致判断, 可稍放宽避免闪烁)
|
||||
if(window.innerWidth>700){ wrap.innerHTML=''; return; }
|
||||
let html='';
|
||||
S.servers.forEach((s,idx)=>{
|
||||
const online = s.online4||s.online6;
|
||||
const proto = online ? (s.online4 && s.online6? '双栈': s.online4? 'IPv4':'IPv6') : '离线';
|
||||
const pill = `<span class="status-pill ${online?'on':'off'}">${proto}</span>`;
|
||||
const memPct = s.memory_total? (s.memory_used/s.memory_total*100):0;
|
||||
const hddPct = s.hdd_total? (s.hdd_used/s.hdd_total*100):0;
|
||||
const monthIn = bytes(s.network_in - s.last_network_in);
|
||||
const monthOut = bytes(s.network_out - s.last_network_out);
|
||||
const netNow = bytes(s.network_rx)+' | '+bytes(s.network_tx);
|
||||
const netTotal = bytes(s.network_in)+' | '+bytes(s.network_out);
|
||||
const p1 = (s.ping_10010||0); const p2=(s.ping_189||0); const p3=(s.ping_10086||0);
|
||||
function bucket(p){ const v=Math.max(0,Math.min(100,p)); const level = v>=20?'bad':(v>=10?'warn':'ok'); return `<div class=\"bucket\" data-lv=\"${level}\"><span style=\"--h:${v}%\"></span><label>${v.toFixed(0)}%</label></div>`; }
|
||||
const buckets = `<div class=\"buckets\">${bucket(p1)}${bucket(p2)}${bucket(p3)}</div>`;
|
||||
const key = s.name || s.location || 'node';
|
||||
html += `<div class=\"card\" data-idx=\"${idx}\">\n <button class=\"expand-btn\" aria-label=\"展开\">▼</button>\n <div class=\"card-header\">\n <div class=\"card-title\">${s.name||'-'} <span class=\"tag\">${s.location||'-'}</span></div>\n ${pill}\n </div>\n <div class=\"kvlist\">\n <div><span class=\"key\">负载</span><span>${s.load_1==-1?'–':s.load_1?.toFixed(2)}</span></div>\n <div><span class=\"key\">在线</span><span>${s.uptime||'-'}</span></div>\n <div><span class=\"key\">月流量</span><span>${monthIn}/${monthOut}</span></div>\n <div><span class=\"key\">网络</span><span>${netNow}</span></div>\n <div><span class=\"key\">总流量</span><span>${netTotal}</span></div>\n <div><span class=\"key\">CPU</span><span>${s.cpu||0}%</span></div>\n <div><span class=\"key\">内存</span><span>${memPct.toFixed(0)}%</span></div>\n <div><span class=\"key\">硬盘</span><span>${hddPct.toFixed(0)}%</span></div>\n </div>\n ${buckets}\n <div class=\"expand-area\">\n <div style=\"font-size:.65rem;opacity:.7;margin-top:.3rem\">点击卡片可查看详情</div>\n </div>\n </div>`;
|
||||
});
|
||||
wrap.innerHTML = html || '<div class="muted" style="font-size:.75rem;text-align:center;padding:1rem;">无数据</div>';
|
||||
wrap.querySelectorAll('.card').forEach(card=>{
|
||||
const idx = parseInt(card.getAttribute('data-idx'));
|
||||
card.addEventListener('click', e=>{ if(e.target.classList.contains('expand-btn')){ card.classList.toggle('expanded'); e.stopPropagation(); return;} openDetail(idx); });
|
||||
});
|
||||
}
|
||||
|
||||
function renderMonitors(){
|
||||
const tbody = els.monitorsBody();
|
||||
let html='';
|
||||
S.servers.forEach(s=>{
|
||||
html += `<tr>
|
||||
<td>${(s.online4||s.online6)?'在线':'离线'}</td>
|
||||
<td>${s.name||'-'}</td>
|
||||
<td>${s.location||'-'}</td>
|
||||
<td>${s.custom||'-'}</td>
|
||||
</tr>`;
|
||||
});
|
||||
tbody.innerHTML = html || `<tr><td colspan="4" class="muted" style="text-align:center;padding:1rem;">无数据</td></tr>`;
|
||||
}
|
||||
|
||||
function renderSSL(){
|
||||
const tbody = els.sslBody();
|
||||
let html='';
|
||||
S.ssl.forEach(c=>{
|
||||
const cls = c.expire_days<=0? 'err': c.expire_days<=7? 'warn':'ok';
|
||||
const status = c.expire_days<=0? '已过期': c.expire_days<=7? '将到期':'正常';
|
||||
const dt = c.expire_ts? new Date(c.expire_ts*1000).toISOString().replace('T',' ').replace(/\.\d+Z/,''):'-';
|
||||
html += `<tr>
|
||||
<td>${c.name||'-'}</td>
|
||||
<td>${(c.domain||'').replace(/^https?:\/\//,'')}</td>
|
||||
<td>${c.port||443}</td>
|
||||
<td><span class="badge ${cls}">${c.expire_days??'-'}</span></td>
|
||||
<td>${dt}</td>
|
||||
<td><span class="badge ${cls}">${status}</span></td>
|
||||
</tr>`;
|
||||
});
|
||||
tbody.innerHTML = html || `<tr><td colspan="6" class="muted" style="text-align:center;padding:1rem;">无证书数据</td></tr>`;
|
||||
}
|
||||
|
||||
function updateTime(){
|
||||
const el = els.last();
|
||||
if(S.updated){ el.textContent = '最后更新: '+ humanAgo(S.updated); }
|
||||
}
|
||||
|
||||
function bindTabs(){
|
||||
document.getElementById('navTabs').addEventListener('click',e=>{
|
||||
if(e.target.tagName!=='BUTTON') return; const tab=e.target.dataset.tab;
|
||||
document.querySelectorAll('.nav button').forEach(b=>b.classList.toggle('active',b===e.target));
|
||||
document.querySelectorAll('.panel').forEach(p=>p.classList.toggle('active', p.id==='panel-'+tab));
|
||||
});
|
||||
}
|
||||
function bindTheme(){
|
||||
const btn = document.getElementById('themeToggle');
|
||||
const mql = window.matchMedia('(prefers-color-scheme: light)');
|
||||
const saved = localStorage.getItem('theme'); // 'light' | 'dark' | null (auto)
|
||||
|
||||
const apply = (isLight)=>{ document.body.classList.toggle('light', isLight); };
|
||||
|
||||
if(!saved){
|
||||
// 自动跟随系统
|
||||
apply(mql.matches);
|
||||
// 监听系统偏好变化(仅在未手动选择时)
|
||||
mql.addEventListener('change', e=>{ if(!localStorage.getItem('theme')) apply(e.matches); });
|
||||
} else {
|
||||
apply(saved==='light');
|
||||
}
|
||||
|
||||
btn.addEventListener('click',()=>{
|
||||
// 用户手动切换后即固定,不再自动
|
||||
const toLight = !document.body.classList.contains('light');
|
||||
apply(toLight);
|
||||
localStorage.setItem('theme', toLight?'light':'dark');
|
||||
});
|
||||
}
|
||||
|
||||
bindTabs();
|
||||
bindTheme();
|
||||
fetchData();
|
||||
setInterval(fetchData, 4000);
|
||||
setInterval(updateTime, 60000);
|
||||
|
||||
// 详情弹窗逻辑
|
||||
function openDetail(i){
|
||||
const s = S.servers[i]; if(!s) return;
|
||||
const box = document.getElementById('detailContent');
|
||||
const modal = document.getElementById('detailModal');
|
||||
document.getElementById('detailTitle').textContent = s.name + ' 详情';
|
||||
const offline = !(s.online4||s.online6);
|
||||
const memLine = `内存: ${s.memory_total? bytes(s.memory_used*1024)+' / '+bytes(s.memory_total*1024):'- / -'} | 虚存: ${s.swap_total? bytes(s.swap_used*1024)+' / '+bytes(s.swap_total*1024):'- / -'}`;
|
||||
const hddLine = `硬盘: ${s.hdd_total? bytes(s.hdd_used*1024*1024)+' / '+bytes(s.hdd_total*1024*1024):'- / -'} | 读/写: ${(typeof s.io_read==='number')? bytes(s.io_read):'-'} / ${(typeof s.io_write==='number')? bytes(s.io_write):'-'}`;
|
||||
const procLine = `${num(s.tcp_count)} / ${num(s.udp_count)} / ${num(s.process_count)} / ${num(s.thread_count)}`;
|
||||
const latText = offline ? '离线' : `CU/CT/CM: ${num(s.time_10010)}ms (${(s.ping_10010||0).toFixed(0)}%) / ${num(s.time_189)}ms (${(s.ping_189||0).toFixed(0)}%) / ${num(s.time_10086)}ms (${(s.ping_10086||0).toFixed(0)}%)`;
|
||||
const key = s.name || s.location || 'node';
|
||||
|
||||
let latencyBlock = '';
|
||||
if(!offline){
|
||||
latencyBlock = `
|
||||
<div class="kv"><span>当前延迟</span><span class="mono">${latText}</span></div>
|
||||
<div style="display:flex;flex-direction:column;gap:.4rem;">
|
||||
<canvas id="latChart" height="150" style="width:100%;border:1px solid var(--border);border-radius:10px;background:linear-gradient(145deg,var(--bg),var(--bg-alt));"></canvas>
|
||||
<div class="mono" style="font-size:11px;display:flex;gap:1rem;flex-wrap:wrap;">
|
||||
<span style="color:#3b82f6">● CU</span>
|
||||
<span style="color:#10b981">● CT</span>
|
||||
<span style="color:#f59e0b">● CM</span>
|
||||
<span style="opacity:.6"> (最近 ~${S.hist[key]?S.hist[key].cu.length:0} 条)</span>
|
||||
</div>
|
||||
</div>`;
|
||||
} else {
|
||||
latencyBlock = `<div class="kv"><span>当前延迟</span><span class="mono">离线,无数据</span></div>`;
|
||||
}
|
||||
|
||||
box.innerHTML = `
|
||||
<div class="kv"><span>位置</span><span class="mono">${s.location||'-'}</span></div>
|
||||
<div class="kv"><span>负载 (1/5/15)</span><span class="mono">${offline?' - / - / -':(s.load_1==-1?'–':s.load_1?.toFixed(2))+' / '+(s.load_5?.toFixed?.(2)||'-')+' / '+(s.load_15?.toFixed?.(2)||'-')}</span></div>
|
||||
<div style="display:flex;flex-direction:column;gap:.35rem;">
|
||||
<canvas id="loadChart" height="120" style="width:100%;border:1px solid var(--border);border-radius:10px;background:linear-gradient(145deg,var(--bg),var(--bg-alt));"></canvas>
|
||||
<div class="mono" style="font-size:11px;opacity:.65;">负载历史 (1/5/15) 最近 ~${(S.loadHist[key]?S.loadHist[key].l1.length:0)} 条</div>
|
||||
<div class="mono" style="font-size:10px;display:flex;gap:.75rem;flex-wrap:wrap;opacity:.6;">
|
||||
<span style="color:#8b5cf6">● 1</span>
|
||||
<span style="color:#10b981">● 5</span>
|
||||
<span style="color:#f59e0b">● 15</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kv"><span>内存|虚存</span><span class="mono">${offline?'- / - | 虚存: - / -':memLine}</span></div>
|
||||
<div class="kv"><span>硬盘|读写</span><span class="mono">${offline?'- / - | 读/写: - / -':hddLine}</span></div>
|
||||
<div class="kv"><span>TCP/UDP/进/线</span><span class="mono">${procLine}</span></div>
|
||||
${latencyBlock}
|
||||
`;
|
||||
modal.style.display='flex';
|
||||
document.addEventListener('keydown', escCloseOnce);
|
||||
if(!offline){
|
||||
drawLatencyChart(key);
|
||||
drawLoadChart(key);
|
||||
S._openDetailKey = key; // 记录当前弹窗对应节点
|
||||
} else {
|
||||
S._openDetailKey = null;
|
||||
}
|
||||
}
|
||||
function escCloseOnce(e){ if(e.key==='Escape'){ closeDetail(); } }
|
||||
function closeDetail(){ const m=document.getElementById('detailModal'); m.style.display='none'; document.removeEventListener('keydown', escCloseOnce); }
|
||||
document.getElementById('detailClose').addEventListener('click', closeDetail);
|
||||
document.getElementById('detailModal').addEventListener('click', e=>{ if(e.target.id==='detailModal') closeDetail(); });
|
||||
|
||||
// 绘制三网延迟折线图 (简易实现)
|
||||
function drawLatencyChart(key){
|
||||
const data = S.hist[key];
|
||||
const canvas = document.getElementById('latChart');
|
||||
if(!canvas || !data) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const W = canvas.clientWidth; const H = canvas.height; canvas.width = W; // 适配宽度
|
||||
ctx.clearRect(0,0,W,H);
|
||||
const padL=40, padR=10, padT=10, padB=18;
|
||||
const series = [ {arr:data.cu,color:'#3b82f6'}, {arr:data.ct,color:'#10b981'}, {arr:data.cm,color:'#f59e0b'} ];
|
||||
const allVals = series.flatMap(s=>s.arr);
|
||||
if(!allVals.length){ ctx.fillStyle='var(--text-dim)'; ctx.font='12px system-ui'; ctx.fillText('暂无数据', W/2-30, H/2); return; }
|
||||
const max = Math.max(...allVals);
|
||||
const min = Math.min(...allVals);
|
||||
const range = Math.max(1, max-min);
|
||||
const n = Math.max(...series.map(s=>s.arr.length));
|
||||
const xStep = (W - padL - padR) / Math.max(1,n-1);
|
||||
// 网格与轴
|
||||
ctx.strokeStyle='rgba(255,255,255,0.08)'; ctx.lineWidth=1;
|
||||
ctx.beginPath(); ctx.moveTo(padL,padT); ctx.lineTo(padL,H-padB); ctx.lineTo(W-padR,H-padB); ctx.stroke();
|
||||
ctx.fillStyle='var(--text-dim)'; ctx.font='10px system-ui';
|
||||
const yMarks=4; for(let i=0;i<=yMarks;i++){ const y = padT + (H-padT-padB)*i/yMarks; const val = (max - range*i/yMarks).toFixed(0)+'ms'; ctx.fillText(val,4,y+3); ctx.strokeStyle='rgba(255,255,255,0.05)'; ctx.beginPath(); ctx.moveTo(padL,y); ctx.lineTo(W-padR,y); ctx.stroke(); }
|
||||
// 绘制线
|
||||
series.forEach(s=>{
|
||||
if(s.arr.length<2) return;
|
||||
ctx.strokeStyle = s.color; ctx.lineWidth=1.6; ctx.beginPath();
|
||||
s.arr.forEach((v,i)=>{ const x = padL + xStep*i; const y = padT + (H-padT-padB)*(1-(v-min)/range); if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); });
|
||||
ctx.stroke();
|
||||
});
|
||||
}
|
||||
|
||||
// 在每次 render 后若弹窗打开则重绘最新图
|
||||
const _oldRender = render;
|
||||
render = function(){ _oldRender(); if(S._openDetailKey){ drawLatencyChart(S._openDetailKey); } };
|
||||
window.addEventListener('resize', ()=>{ if(S._openDetailKey){ drawLatencyChart(S._openDetailKey); drawLoadChart(S._openDetailKey); } });
|
||||
|
||||
// 绘制小型折线 (sparklines)
|
||||
function drawSparks(){
|
||||
const els = document.querySelectorAll('.spark');
|
||||
els.forEach(div=>{
|
||||
// 若已有canvas跳过重建
|
||||
let canvas = div.querySelector('canvas');
|
||||
if(!canvas){ canvas = document.createElement('canvas'); div.appendChild(canvas); }
|
||||
const key = div.getAttribute('data-key');
|
||||
const metric = div.getAttribute('data-metric');
|
||||
const hist = (S.metricHist[key] && S.metricHist[key][metric])? S.metricHist[key][metric]:[];
|
||||
const W = 80, H = 26; canvas.width=W; canvas.height=H; const ctx=canvas.getContext('2d');
|
||||
ctx.clearRect(0,0,W,H);
|
||||
div.classList.add('spark-ready');
|
||||
if(hist.length<2){ ctx.fillStyle='var(--text-dim)'; ctx.font='10px system-ui'; ctx.fillText('-', W/2-3, H/2+3); return; }
|
||||
const max = Math.max(...hist); const min = Math.min(...hist); const range = Math.max(1,max-min);
|
||||
const step = W/(hist.length-1);
|
||||
// 线颜色
|
||||
let color = '#3b82f6'; if(metric==='mem') color='#10b981'; else if(metric==='hdd') color='#f59e0b';
|
||||
ctx.strokeStyle=color; ctx.lineWidth=1.3; ctx.beginPath();
|
||||
hist.forEach((v,i)=>{ const x=i*step; const y=H - ( (v-min)/range )* (H-4) -2; if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); });
|
||||
ctx.stroke();
|
||||
// 当前值点
|
||||
const last = hist[hist.length-1]; const lx = W-1; const ly = H - ((last-min)/range)*(H-4)-2; ctx.fillStyle=color; ctx.beginPath(); ctx.arc(lx,ly,2,0,Math.PI*2); ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
// 负载折线图 (load1 历史)
|
||||
function drawLoadChart(key){
|
||||
const L = S.loadHist[key];
|
||||
const canvas = document.getElementById('loadChart');
|
||||
if(!canvas) return; const ctx = canvas.getContext('2d');
|
||||
if(!L){ ctx.clearRect(0,0,canvas.width,canvas.height); return; }
|
||||
const l1=L.l1||[], l5=L.l5||[], l15=L.l15||[];
|
||||
const canvasW = canvas.clientWidth; const H = canvas.height; canvas.width = canvasW; const W=canvasW;
|
||||
ctx.clearRect(0,0,W,H);
|
||||
if(l1.length<2){ ctx.fillStyle='var(--text-dim)'; ctx.font='12px system-ui'; ctx.fillText('暂无负载数据', W/2-42, H/2); return; }
|
||||
const all = [...l1,...l5,...l15];
|
||||
const padL=38,padR=8,padT=8,padB=16;
|
||||
const max=Math.max(...all); const min=Math.min(...all); const range=Math.max(0.5,max-min);
|
||||
const n = Math.max(l1.length,l5.length,l15.length); const xStep=(W-padL-padR)/Math.max(1,n-1);
|
||||
// 轴 & 网格
|
||||
ctx.strokeStyle='rgba(255,255,255,0.08)'; ctx.lineWidth=1; ctx.beginPath(); ctx.moveTo(padL,padT); ctx.lineTo(padL,H-padB); ctx.lineTo(W-padR,H-padB); ctx.stroke();
|
||||
ctx.fillStyle='var(--text-dim)'; ctx.font='10px system-ui';
|
||||
const yMarks=4; for(let i=0;i<=yMarks;i++){ const y=padT+(H-padT-padB)*i/yMarks; const val=(max - range*i/yMarks).toFixed(2); ctx.fillText(val,4,y+3); ctx.strokeStyle='rgba(255,255,255,0.05)'; ctx.beginPath(); ctx.moveTo(padL,y); ctx.lineTo(W-padR,y); ctx.stroke(); }
|
||||
const series=[{arr:l1,color:'#8b5cf6',fill:true},{arr:l5,color:'#10b981'},{arr:l15,color:'#f59e0b'}];
|
||||
// 面积先画 load1
|
||||
series.forEach(s=>{
|
||||
if(s.arr.length<2) return;
|
||||
ctx.beginPath(); ctx.lineWidth=1.5; ctx.strokeStyle=s.color;
|
||||
s.arr.forEach((v,i)=>{ const x=padL+xStep*i; const y=padT+(H-padT-padB)*(1-(v-min)/range); if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); });
|
||||
ctx.stroke();
|
||||
if(s.fill){
|
||||
const lastX = padL + xStep*(s.arr.length-1);
|
||||
ctx.lineTo(lastX,H-padB); ctx.lineTo(padL,H-padB); ctx.closePath();
|
||||
const grd = ctx.createLinearGradient(0,padT,0,H-padB); grd.addColorStop(0,'rgba(139,92,246,0.25)'); grd.addColorStop(1,'rgba(139,92,246,0)');
|
||||
ctx.fillStyle=grd; ctx.fill();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//# sourceMappingURL=app.js.map
|
||||
7
web/js/bootstrap.bundle.min.js
vendored
7
web/js/bootstrap.bundle.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,403 +0,0 @@
|
||||
// serverstatus.js. big data boom today.
|
||||
var error = 0;
|
||||
var d = 0;
|
||||
var server_status = new Array();
|
||||
|
||||
function timeSince(date) {
|
||||
if (date == 0) return "从未.";
|
||||
var seconds = Math.floor((new Date() - date) / 1000);
|
||||
var interval = Math.floor(seconds / 60);
|
||||
return interval > 1 ? interval + " 分钟前." : "几秒前.";
|
||||
}
|
||||
|
||||
function bytesToSize(bytes, precision, si = false) {
|
||||
const units = si ? ['B', 'KB', 'MB', 'GB', 'TB'] : ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = si ? 1000 : 1024;
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(precision)) + ' ' + units[i];
|
||||
}
|
||||
|
||||
function uptime() {
|
||||
fetch("json/stats.json")
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(result => {
|
||||
document.getElementById("loading-notice")?.remove();
|
||||
if (result.reload) setTimeout(location.reload, 1000);
|
||||
|
||||
// 构建 SSL 证书映射
|
||||
const sslMap = {};
|
||||
if (Array.isArray(result.sslcerts)) {
|
||||
result.sslcerts.forEach(c => {
|
||||
if (c.domain) {
|
||||
const d = c.domain.replace(/^https?:\/\//,'').replace(/[:/].*/,'');
|
||||
sslMap[d] = {...c, domain_clean:d};
|
||||
}
|
||||
});
|
||||
// 渲染独立 SSL 面板
|
||||
const tbody = document.getElementById('sslcerts');
|
||||
if (tbody) {
|
||||
tbody.innerHTML = '';
|
||||
result.sslcerts.forEach((raw, idx) => {
|
||||
const c = {...raw};
|
||||
const clean = (c.domain||'').replace(/^https?:\/\//,'').replace(/[:/].*/,'');
|
||||
c.domain_clean = clean;
|
||||
const days = c.expire_days;
|
||||
let cls = 'text-success';
|
||||
let status = '正常';
|
||||
// 先判定过期
|
||||
if (days <= 0) { cls = 'text-danger fw-bold'; status='已过期'; }
|
||||
else if (c.mismatch) { cls = 'text-danger'; status='域名不匹配'; }
|
||||
else if (days <= 1) { cls = 'text-danger'; status='紧急(≤1天)'; }
|
||||
else if (days <= 3) { cls = 'text-danger'; status='紧急(≤3天)'; }
|
||||
else if (days <= 7) { cls = 'text-warning'; status='将到期'; }
|
||||
const expireDt = c.expire_ts ? new Date(c.expire_ts * 1000).toISOString().replace('T',' ').replace(/\.\d+Z/,'') : '-';
|
||||
tbody.insertAdjacentHTML('beforeend', `
|
||||
<tr id="ssl_${idx}">
|
||||
<td style="text-align:center;">${c.name||'-'}</td>
|
||||
<td>${clean}</td>
|
||||
<td style="text-align:center;">${c.port||443}</td>
|
||||
<td style="text-align:center;" class="${cls}">${days ?? '-'}</td>
|
||||
<td style="text-align:center;">${expireDt}</td>
|
||||
<td style="text-align:center;" class="${cls}">${status}</td>
|
||||
</tr>`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
result.servers.forEach((server, i) => {
|
||||
let TableRow = document.querySelector(`#servers tr#r${i}`);
|
||||
let MableRow = document.querySelector(`#monitors tr#r${i}`);
|
||||
let ExpandRow = document.querySelector(`#servers #rt${i}`);
|
||||
let hack = i % 2 ? "odd" : "even";
|
||||
|
||||
if (!TableRow) {
|
||||
document.getElementById("servers").insertAdjacentHTML(
|
||||
"beforeend",
|
||||
`<tr id="r${i}" data-bs-toggle="collapse" data-bs-target="#rt${i}" class="accordion-toggle ${hack}">
|
||||
<td id="online_status"><div class="progress"><div style="width: 100%;" class="progress-bar bg-warning"><small>加载中</small></div></div></td>
|
||||
<td id="month_traffic"><div class="progress"><div style="width: 100%;" class="progress-bar bg-warning"><small>加载中</small></div></div></td>
|
||||
<td id="name">加载中</td>
|
||||
<td id="type">加载中</td>
|
||||
<td id="location">加载中</td>
|
||||
<td id="uptime">加载中</td>
|
||||
<td id="load">加载中</td>
|
||||
<td id="network">加载中</td>
|
||||
<td id="traffic">加载中</td>
|
||||
<td id="cpu"><div class="progress"><div style="width: 100%;" class="progress-bar bg-warning"><small>加载中</small></div></div></td>
|
||||
<td id="memory"><div class="progress"><div style="width: 100%;" class="progress-bar bg-warning"><small>加载中</small></div></div></td>
|
||||
<td id="hdd"><div class="progress"><div style="width: 100%;" class="progress-bar bg-warning"><small>加载中</small></div></div></td>
|
||||
<td id="ping"><div class="progress"><div style="width: 100%;" class="progress-bar bg-warning"><small>加载中</small></div></div></td>
|
||||
</tr>
|
||||
<tr class="expandRow ${hack}"><td colspan="16"><div class="accordian-body collapse" id="rt${i}">
|
||||
<div id="expand_mem">加载中</div>
|
||||
<div id="expand_hdd">加载中</div>
|
||||
<div id="expand_tupd">加载中</div>
|
||||
<div id="expand_ping">加载中</div>
|
||||
</div></td></tr>`
|
||||
);
|
||||
TableRow = document.querySelector(`#servers tr#r${i}`);
|
||||
ExpandRow = document.querySelector(`#servers #rt${i}`);
|
||||
server_status[i] = true;
|
||||
}
|
||||
|
||||
if (!MableRow) {
|
||||
document.getElementById("monitors").insertAdjacentHTML(
|
||||
"beforeend",
|
||||
`<tr id="r${i}" data-bs-target="#rt${i}" class="accordion-toggle ${hack}">
|
||||
<td id="monitor_status"><div class="progress"><div style="width: 100%;" class="progress-bar bg-warning"><small>加载中</small></div></div></td>
|
||||
<td id="monitor_node">加载中</td>
|
||||
<td id="monitor_location">加载中</td>
|
||||
<td id="monitor_text">加载中</td>
|
||||
</tr>`
|
||||
);
|
||||
MableRow = document.querySelector(`#monitors tr#r${i}`);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
TableRow.setAttribute("data-bs-target", `#rt${i}`);
|
||||
MableRow.setAttribute("data-bs-target", `#rt${i}`);
|
||||
server_status[i] = true;
|
||||
}
|
||||
|
||||
const statusClass = server.online4 || server.online6 ? "progress-bar bg-success" : "progress-bar bg-danger";
|
||||
const statusText = server.online4 && server.online6 ? "双栈" : server.online4 ? "IPv4" : server.online6 ? "IPv6" : "关闭";
|
||||
|
||||
if (TableRow) {
|
||||
const onlineStatusBar = TableRow.querySelector("#online_status .progress-bar");
|
||||
if (onlineStatusBar) {
|
||||
onlineStatusBar.setAttribute("class", statusClass);
|
||||
onlineStatusBar.innerHTML = `<small>${statusText}</small>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (MableRow) {
|
||||
const monitorStatusBar = MableRow.querySelector("#monitor_status .progress-bar");
|
||||
if (monitorStatusBar) {
|
||||
monitorStatusBar.setAttribute("class", statusClass);
|
||||
monitorStatusBar.innerHTML = `<small>${statusText}</small>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (TableRow) {
|
||||
TableRow.querySelector("#name").innerHTML = server.name;
|
||||
TableRow.querySelector("#type").innerHTML = server.type;
|
||||
TableRow.querySelector("#location").innerHTML = server.location;
|
||||
}
|
||||
|
||||
if (MableRow) {
|
||||
MableRow.querySelector("#monitor_node").innerHTML = server.name;
|
||||
MableRow.querySelector("#monitor_location").innerHTML = server.location;
|
||||
}
|
||||
|
||||
if (!server.online4 && !server.online6) {
|
||||
if (server_status[i]) {
|
||||
if (TableRow) {
|
||||
TableRow.querySelector("#uptime").innerHTML = "–";
|
||||
TableRow.querySelector("#load").innerHTML = "–";
|
||||
TableRow.querySelector("#network").innerHTML = "–";
|
||||
TableRow.querySelector("#traffic").innerHTML = "–";
|
||||
const monthTrafficBar = TableRow.querySelector("#month_traffic .progress-bar");
|
||||
if (monthTrafficBar) {
|
||||
monthTrafficBar.setAttribute("class", "progress-bar bg-warning");
|
||||
monthTrafficBar.innerHTML = "<small>关闭</small>";
|
||||
}
|
||||
const cpuBar = TableRow.querySelector("#cpu .progress-bar");
|
||||
if (cpuBar) {
|
||||
cpuBar.setAttribute("class", "progress-bar bg-danger");
|
||||
cpuBar.style.width = "100%";
|
||||
cpuBar.innerHTML = "<small>关闭</small>";
|
||||
}
|
||||
const memoryBar = TableRow.querySelector("#memory .progress-bar");
|
||||
if (memoryBar) {
|
||||
memoryBar.setAttribute("class", "progress-bar bg-danger");
|
||||
memoryBar.style.width = "100%";
|
||||
memoryBar.innerHTML = "<small>关闭</small>";
|
||||
}
|
||||
const hddBar = TableRow.querySelector("#hdd .progress-bar");
|
||||
if (hddBar) {
|
||||
hddBar.setAttribute("class", "progress-bar bg-danger");
|
||||
hddBar.style.width = "100%";
|
||||
hddBar.innerHTML = "<small>关闭</small>";
|
||||
}
|
||||
const pingBar = TableRow.querySelector("#ping .progress-bar");
|
||||
if (pingBar) {
|
||||
pingBar.setAttribute("class", "progress-bar bg-danger");
|
||||
pingBar.style.width = "100%";
|
||||
pingBar.innerHTML = "<small>关闭</small>";
|
||||
}
|
||||
// SSL 列已移除
|
||||
}
|
||||
if (MableRow) {
|
||||
MableRow.querySelector("#monitor_text").innerHTML = "-";
|
||||
}
|
||||
if (ExpandRow && ExpandRow.classList.contains("show")) ExpandRow.classList.remove("show");
|
||||
if (TableRow) TableRow.setAttribute("data-bs-target", "");
|
||||
if (MableRow) MableRow.setAttribute("data-bs-target", "");
|
||||
server_status[i] = false;
|
||||
}
|
||||
} else {
|
||||
if (!server_status[i]) {
|
||||
if (TableRow) TableRow.setAttribute("data-bs-target", `#rt${i}`);
|
||||
if (MableRow) MableRow.setAttribute("data-bs-target", `#rt${i}`);
|
||||
server_status[i] = true;
|
||||
}
|
||||
|
||||
const trafficdiff_in = server.network_in - server.last_network_in;
|
||||
const trafficdiff_out = server.network_out - server.last_network_out;
|
||||
const monthtraffic = `${bytesToSize(trafficdiff_in, 1, true)} | ${bytesToSize(trafficdiff_out, 1, true)}`;
|
||||
if (TableRow) {
|
||||
const monthTrafficBar = TableRow.querySelector("#month_traffic .progress-bar");
|
||||
if (monthTrafficBar) {
|
||||
monthTrafficBar.setAttribute("class", "progress-bar bg-success");
|
||||
monthTrafficBar.innerHTML = `<small>${monthtraffic}</small>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (TableRow) TableRow.querySelector("#uptime").innerHTML = server.uptime;
|
||||
if (TableRow) TableRow.querySelector("#load").innerHTML = server.load_1 == -1 ? "–" : server.load_1.toFixed(2);
|
||||
|
||||
const netstr = `${bytesToSize(server.network_rx, 1, true)} | ${bytesToSize(server.network_tx, 1, true)}`;
|
||||
if (TableRow) TableRow.querySelector("#network").innerHTML = netstr;
|
||||
|
||||
const trafficstr = `${bytesToSize(server.network_in, 1, true)} | ${bytesToSize(server.network_out, 1, true)}`;
|
||||
if (TableRow) TableRow.querySelector("#traffic").innerHTML = trafficstr;
|
||||
|
||||
const cpuClass = server.cpu >= 90 ? "progress-bar bg-danger" : server.cpu >= 80 ? "progress-bar bg-warning" : "progress-bar bg-success";
|
||||
if (TableRow) {
|
||||
const cpuBar = TableRow.querySelector("#cpu .progress-bar");
|
||||
if (cpuBar) {
|
||||
cpuBar.setAttribute("class", cpuClass);
|
||||
cpuBar.style.width = `${server.cpu}%`;
|
||||
cpuBar.innerHTML = `${server.cpu}%`;
|
||||
}
|
||||
}
|
||||
|
||||
const Mem = ((server.memory_used / server.memory_total) * 100).toFixed(0);
|
||||
const memClass = Mem >= 90 ? "progress-bar bg-danger" : Mem >= 80 ? "progress-bar bg-warning" : "progress-bar bg-success";
|
||||
if (TableRow) {
|
||||
const memoryBar = TableRow.querySelector("#memory .progress-bar");
|
||||
if (memoryBar) {
|
||||
memoryBar.setAttribute("class", memClass);
|
||||
memoryBar.style.width = `${Mem}%`;
|
||||
memoryBar.innerHTML = `${Mem}%`;
|
||||
}
|
||||
}
|
||||
if (ExpandRow) ExpandRow.querySelector("#expand_mem").innerHTML = `内存|虚存: ${bytesToSize(server.memory_used * 1024, 1)} / ${bytesToSize(server.memory_total * 1024, 1)} | ${bytesToSize(server.swap_used * 1024, 0)} / ${bytesToSize(server.swap_total * 1024, 0)}`;
|
||||
|
||||
const HDD = ((server.hdd_used / server.hdd_total) * 100).toFixed(0);
|
||||
const hddClass = HDD >= 90 ? "progress-bar bg-danger" : HDD >= 80 ? "progress-bar bg-warning" : "progress-bar bg-success";
|
||||
if (TableRow) {
|
||||
const hddBar = TableRow.querySelector("#hdd .progress-bar");
|
||||
if (hddBar) {
|
||||
hddBar.setAttribute("class", hddClass);
|
||||
hddBar.style.width = `${HDD}%`;
|
||||
hddBar.innerHTML = `${HDD}%`;
|
||||
}
|
||||
}
|
||||
const io = `${bytesToSize(server.io_read, 0, true)} / ${bytesToSize(server.io_write, 0, true)}`;
|
||||
if (ExpandRow) ExpandRow.querySelector("#expand_hdd").innerHTML = `硬盘|读写: ${bytesToSize(server.hdd_used * 1024 * 1024, 2)} / ${bytesToSize(server.hdd_total * 1024 * 1024, 2)} | ${io}`;
|
||||
|
||||
if (ExpandRow) ExpandRow.querySelector("#expand_tupd").innerHTML = `TCP/UDP/进/线: ${server.tcp_count} / ${server.udp_count} / ${server.process_count} / ${server.thread_count}`;
|
||||
|
||||
const PING_10010 = server.ping_10010.toFixed(0);
|
||||
const PING_189 = server.ping_189.toFixed(0);
|
||||
const PING_10086 = server.ping_10086.toFixed(0);
|
||||
const pingClass = PING_10010 >= 20 || PING_189 >= 20 || PING_10086 >= 20 ? "progress-bar bg-danger" : PING_10010 >= 10 || PING_189 >= 10 || PING_10086 >= 10 ? "progress-bar bg-warning" : "progress-bar bg-success";
|
||||
if (TableRow) {
|
||||
const pingBar = TableRow.querySelector("#ping .progress-bar");
|
||||
if (pingBar) {
|
||||
pingBar.setAttribute("class", pingClass);
|
||||
pingBar.innerHTML = `${PING_10010}%💻${PING_189}%💻${PING_10086}%`;
|
||||
}
|
||||
}
|
||||
if (ExpandRow) ExpandRow.querySelector("#expand_ping").innerHTML = `CU/CT/CM: ${server.time_10010}ms (${PING_10010}%) / ${server.time_189}ms (${PING_189}%) / ${server.time_10086}ms (${PING_10086}%)`;
|
||||
|
||||
if (MableRow) MableRow.querySelector("#monitor_text").innerHTML = server.custom;
|
||||
|
||||
// SSL 匹配: 使用 host 的域名部分匹配 sslMap
|
||||
const extractDomain = (h) => {
|
||||
if(!h) return '';
|
||||
return h.replace(/^https?:\/\//,'').replace(/:.*/,'');
|
||||
};
|
||||
const hostDomain = extractDomain(server.host || server.name || '');
|
||||
// 首页服务器表已取消 SSL 列
|
||||
// 服务表移除 SSL 列
|
||||
}
|
||||
});
|
||||
|
||||
d = new Date(result.updated * 1000);
|
||||
error = 0;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Fetch error: ", error);
|
||||
if (!error) {
|
||||
document.querySelectorAll("#servers > tr.accordion-toggle").forEach((TableRow, i) => {
|
||||
const MableRow = document.querySelector(`#monitors tr#r${i}`);
|
||||
const ExpandRow = document.querySelector(`#servers #rt${i}`);
|
||||
|
||||
if (TableRow && MableRow) {
|
||||
TableRow.querySelectorAll(".progress-bar").forEach(bar => {
|
||||
if (bar) {
|
||||
bar.setAttribute("class", "progress-bar bg-danger");
|
||||
bar.innerHTML = "<small>错误</small>";
|
||||
}
|
||||
});
|
||||
|
||||
MableRow.querySelectorAll(".progress-bar").forEach(bar => {
|
||||
if (bar) {
|
||||
bar.setAttribute("class", "progress-bar bg-danger");
|
||||
bar.innerHTML = "<small>错误</small>";
|
||||
}
|
||||
});
|
||||
|
||||
if (ExpandRow && ExpandRow.classList.contains("show")) {
|
||||
ExpandRow.classList.remove("show");
|
||||
}
|
||||
|
||||
TableRow.setAttribute("data-bs-target", "");
|
||||
MableRow.setAttribute("data-bs-target", "");
|
||||
server_status[i] = false;
|
||||
} else {
|
||||
console.error(`TableRow or MableRow is undefined for index ${i}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
error = 1;
|
||||
document.getElementById("updated").innerHTML = "更新错误.";
|
||||
});
|
||||
}
|
||||
|
||||
function updateTime() {
|
||||
if (!error) document.getElementById("updated").innerHTML = "最后更新: " + timeSince(d);
|
||||
}
|
||||
|
||||
uptime();
|
||||
updateTime();
|
||||
// 降低改值,可以减少cpu占用
|
||||
setInterval(uptime, 2000);
|
||||
setInterval(updateTime, 2000);
|
||||
|
||||
// styleswitcher.js
|
||||
function setActiveStyleSheet(title) {
|
||||
var i, a, main;
|
||||
for (i = 0; (a = document.getElementsByTagName("link")[i]); i++) {
|
||||
if (a.getAttribute("rel").indexOf("stylesheet") != -1 && a.getAttribute("title")) {
|
||||
a.disabled = true;
|
||||
if (a.getAttribute("title") == title) a.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getActiveStyleSheet() {
|
||||
return Array.from(document.getElementsByTagName("link")).find(a => a.getAttribute("rel").includes("style") && a.getAttribute("title") && !a.disabled)?.getAttribute("title") || null;
|
||||
}
|
||||
|
||||
function createCookie(name, value, days) {
|
||||
const expires = days ? `; expires=${new Date(Date.now() + days * 24 * 60 * 60 * 1000).toGMTString()}` : "";
|
||||
document.cookie = `${name}=${value}${expires}; path=/`;
|
||||
}
|
||||
|
||||
function readCookie(name) {
|
||||
const nameEQ = name + "=";
|
||||
const ca = document.cookie.split(';');
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) == ' ') c = c.substring(1, c.length);
|
||||
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
const cookie = readCookie("style");
|
||||
if (cookie && cookie != 'null') {
|
||||
setActiveStyleSheet(cookie);
|
||||
} else {
|
||||
const handleChange = mediaQueryListEvent => setActiveStyleSheet(mediaQueryListEvent.matches ? 'dark' : 'light');
|
||||
const mediaQueryListDark = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
setActiveStyleSheet(mediaQueryListDark.matches ? 'dark' : 'light');
|
||||
mediaQueryListDark.addEventListener("change", handleChange);
|
||||
}
|
||||
|
||||
// 处理标签页切换
|
||||
const tabs = document.querySelectorAll('.nav-link');
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', function(event) {
|
||||
if (this.id === 'navbarDropdown') {
|
||||
return; // 阻止“风格”标签的默认行为
|
||||
}
|
||||
event.preventDefault();
|
||||
const target = this.getAttribute('href');
|
||||
document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.remove('show', 'active'));
|
||||
document.querySelector(target).classList.add('show', 'active');
|
||||
tabs.forEach(t => t.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user