基于 https://www.nodeseek.com/post-348129-1 帖子的 nezha美化修改 不占用上下行流量统计位置 只将其插入到下方 可能有bug,只提供参考,有能力的话自己在改改 加入了进度条颜色显示预警功能,请求刷新间隔改为5分钟一次 可调整 insertPosition: ‘before’, // 可选值:‘after’, ‘before’, ‘replace’
`<script> ;(function () { let trafficTimer = null; let trafficCache = null; let updateScheduled = false;
const config = { showTrafficStats: true, interval: 300000, // 5 分钟 };
function formatFileSize(bytes) { if (bytes === 0) return { value: ‘0’, unit: ‘B’ }; const units = [‘B’, ‘KB’, ‘MB’, ‘GB’, ‘TB’, ‘PB’]; let unitIndex = 0; let size = bytes; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return { value: size.toFixed(unitIndex === 0 ? 0 : 2), unit: units[unitIndex] }; }
function calculatePercentage(used, total) used = Number(used); total = Number(total); if (used > 1e15 return Math.min((used / total * 100), 999).toFixed(1); }
function formatDate(dateString) { const date = new Date(dateString); return date.toLocaleDateString(‘zh-CN’, { year: ‘numeric’, month: ‘2-digit’, day: ‘2-digit’ }); }
function safeSetTextContent(parent, selector, text) { const el = parent.querySelector(selector); if (el) el.textContent = text; }
// 返回进度条的颜色渐变,从绿(0%)到红(100%)
function getGradientColor(percentage) {
const p = Math.min(Math.max(Number(percentage), 0), 100);
// 绿色到红色的线性插值
// 绿 rgb(16, 185, 129) #10b981
// 红 rgb(239, 68, 68) #ef4444
const r = Math.round(16 + ((239 - 16) * p) / 100);
const g = Math.round(185 + ((68 - 185) * p) / 100);
const b = Math.round(129 + ((68 - 129) * p) / 100);
return rgb(${r}, ${g}, ${b})
;
}
const elementCache = new Map();
function renderTrafficStats(trafficData) { const serverMap = new Map();
for (const cycleId in trafficData) {
const cycle = trafficData[cycleId];
if (!cycle.server_name || !cycle.transfer) continue;
for (const serverId in cycle.server_name) {
const serverName = cycle.server_name[serverId];
const transfer = cycle.transfer[serverId];
const max = cycle.max;
const from = cycle.from;
const to = cycle.to;
if (serverName && transfer !== undefined && max && from && to) {
serverMap.set(serverName, {
id: serverId,
transfer,
max,
name: cycle.name,
from,
to
});
}
}
}
serverMap.forEach((serverData, serverName) => {
let targetElement = elementCache.get(serverName);
if (!targetElement) {
targetElement = Array.from(document.querySelectorAll('section.grid.items-center.gap-2'))
.find(el => el.textContent.trim().includes(serverName));
if (!targetElement) return;
elementCache.set(serverName, targetElement);
}
const usedFormatted = formatFileSize(serverData.transfer);
const totalFormatted = formatFileSize(serverData.max);
const percentage = calculatePercentage(serverData.transfer, serverData.max);
const fromFormatted = formatDate(serverData.from);
const toFormatted = formatDate(serverData.to);
const uniqueClassName = 'traffic-stats-for-server-' + serverData.id;
const progressColor = getGradientColor(percentage);
const containerDiv = targetElement.closest('div');
if (!containerDiv) return;
let existing = containerDiv.querySelector('.' + uniqueClassName);
if (!config.showTrafficStats) {
if (existing) existing.remove();
return;
}
if (existing) {
safeSetTextContent(existing, '.used-traffic', usedFormatted.value);
safeSetTextContent(existing, '.used-unit', usedFormatted.unit);
safeSetTextContent(existing, '.total-traffic', totalFormatted.value);
safeSetTextContent(existing, '.total-unit', totalFormatted.unit);
safeSetTextContent(existing, '.from-date', fromFormatted);
safeSetTextContent(existing, '.to-date', toFormatted);
const progressBar = existing.querySelector('.progress-bar');
if (progressBar) {
progressBar.style.width = `${Math.min(percentage, 100)}%`;
progressBar.style.backgroundColor = progressColor;
}
} else {
const oldSection = containerDiv.querySelector('section.flex.items-center.w-full.justify-between.gap-1') ||
containerDiv.querySelector('section.grid.grid-cols-5.items-center.gap-3');
if (!oldSection) return;
const newElement = document.createElement('div');
newElement.classList.add('space-y-1.5', 'new-inserted-element', uniqueClassName);
newElement.style.width = '100%';
newElement.innerHTML = `
<div class="flex items-center justify-between">
<div class="flex items-baseline gap-1">
<span class="text-[10px] font-medium text-neutral-800 dark:text-neutral-200 used-traffic">${usedFormatted.value}</span>
<span class="text-[10px] font-medium text-neutral-800 dark:text-neutral-200 used-unit">${usedFormatted.unit}</span>
<span class="text-[10px] text-neutral-500 dark:text-neutral-400">/ </span>
<span class="text-[10px] text-neutral-500 dark:text-neutral-400 total-traffic">${totalFormatted.value}</span>
<span class="text-[10px] text-neutral-500 dark:text-neutral-400 total-unit">${totalFormatted.unit}</span>
</div>
<div class="text-[10px] font-medium text-neutral-600 dark:text-neutral-300">
<span class="from-date">${fromFormatted}</span>
<span class="text-neutral-500 dark:text-neutral-400">-</span>
<span class="to-date">${toFormatted}</span>
</div>
</div>
<div class="relative h-1.5 overflow-hidden rounded-full bg-neutral-100 dark:bg-neutral-800">
<div class="absolute inset-0 rounded-full transition-all duration-700 ease-out progress-bar" style="width: ${Math.min(percentage, 100)}%; background-color: ${progressColor};"></div>
</div>
`;
oldSection.after(newElement);
}
});
}
function throttleUpdate() { if (updateScheduled) return; updateScheduled = true; setTimeout(() => { updateScheduled = false; updateTrafficStats(); }, 1000); }
function updateTrafficStats(force = false) { const now = Date.now(); if (!force && trafficCache && (now - trafficCache.timestamp < config.interval)) { renderTrafficStats(trafficCache.data); return; }
fetch('/api/v1/service')
.then(res => res.json())
.then(data => {
if (!data.success) return;
const trafficData = data.data.cycle_transfer_stats;
trafficCache = { timestamp: now, data: trafficData };
renderTrafficStats(trafficData);
})
.catch(err => console.error('获取流量失败:', err));
}
function startPeriodicRefresh() { if (!trafficTimer) { trafficTimer = setInterval(() => { updateTrafficStats(); }, config.interval); } }
const targetNode = document.querySelector(‘main’) || document.body; const observer = new MutationObserver(() => { throttleUpdate(); });
observer.observe(targetNode, { childList: true, subtree: true });
updateTrafficStats(true); startPeriodicRefresh();
window.addEventListener(‘beforeunload’, () => { if (trafficTimer) clearInterval(trafficTimer); observer.disconnect(); }); })(); </script> `
全部复制 去掉开头
符号和结尾的
符号