基于 https://www.nodeseek.com/post-348129-1 大佬帖子的nezha美化修改
不占用上下行流量统计位置 只将其插入到下方
加入了进度条颜色显示预警功能 加入了统计时间名称渐变变化功能
可调整 insertPosition: 'before', // 可选值:'after', 'before', 'replace'

<script>
;(function () {
// 定时器和缓存变量
let trafficTimer = null;
let trafficCache = null;
// 配置参数
const config = {
showTrafficStats: true, // 是否显示流量统计
insertPosition: 'after', // 插入位置
interval: 60000, // 刷新间隔(毫秒)
style: 1 // 备用样式参数(未使用)
};
// 格式化字节数为合适单位并返回值和单位
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;
// 根据字节数除以1024递进单位
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return {
value: size.toFixed(unitIndex === 0 ? 0 : 2),
unit: units[unitIndex]
};
}
// 计算使用流量占总量的百分比,返回字符串,保留1位小数
function calculatePercentage(used, total) {
used = Number(used);
total = Number(total);
// 过大数字做缩放避免数值溢出
if (used > 1e15 || total > 1e15) {
used /= 1e10;
total /= 1e10;
}
return (used / total * 100).toFixed(1);
}
// 将日期字符串转为本地格式日期字符串(yyyy-mm-dd)
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;
}
// 根据百分比生成渐变颜色(绿色到红色)
function getGradientColor(percentage) {
const clamp = (val, min, max) => Math.min(Math.max(val, min), max);
const lerp = (start, end, t) => Math.round(start + (end - start) * t);
const p = clamp(Number(percentage), 0, 100) / 100;
const startColor = { r: 16, g: 185, b: 129 }; // 绿色
const endColor = { r: 239, g: 68, b: 68 }; // 红色
const r = lerp(startColor.r, endColor.r, p);
const g = lerp(startColor.g, endColor.g, p);
const b = lerp(startColor.b, endColor.b, p);
return `rgb(${r}, ${g}, ${b})`;
}
// 元素内容淡出、替换、再淡入动画,duration 单位毫秒
function fadeOutIn(element, newContent, duration = 500) {
// 先淡出
element.style.transition = `opacity ${duration / 2}ms`;
element.style.opacity = '0';
setTimeout(() => {
// 替换内容
element.innerHTML = newContent;
// 再淡入
element.style.transition = `opacity ${duration / 2}ms`;
element.style.opacity = '1';
}, duration / 2);
}
// 渲染流量统计条和相关信息
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;
const next_update = cycle.next_update[serverId];
if (serverName && transfer !== undefined && max && from && to) {
serverMap.set(serverName, {
id: serverId,
transfer: transfer,
max: max,
name: cycle.name,
from: from,
to: to,
next_update: next_update
});
}
}
}
// 针对每个服务器找到对应卡片并更新或插入流量统计
serverMap.forEach((serverData, serverName) => {
// 找到显示该服务器名的元素(标题匹配)
const targetElement = Array.from(document.querySelectorAll('section.grid.items-center.gap-2'))
.find(section => {
const firstText = section.querySelector('p.break-all.font-bold.tracking-tight.text-xs')?.textContent.trim();
return firstText === serverName;
});
if (!targetElement) return;
// 格式化显示数据
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 next_update = new Date(serverData.next_update).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
const uniqueClassName = 'traffic-stats-for-server-' + serverData.id;
const progressColor = getGradientColor(percentage);
const containerDiv = targetElement.closest('div');
if (!containerDiv) return;
// 检查是否已有插入元素
const existing = Array.from(containerDiv.querySelectorAll('.new-inserted-element')).find(el =>
el.classList.contains(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, '.percentage-value', percentage + '%');
safeSetTextContent(existing, '.next-update', `next update: ${next_update}`);
const progressBar = existing.querySelector('.progress-bar');
if (progressBar) {
progressBar.style.width = percentage + '%';
progressBar.style.backgroundColor = progressColor;
}
} else {
// 不存在则新建
let oldSection = containerDiv.querySelector('section.flex.items-center.w-full.justify-between.gap-1')
|| containerDiv.querySelector('section.grid.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%';
// 流量条及时间显示HTML结构
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 time-info" style="opacity:1; transition: opacity 0.3s;">
<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">
<div class="absolute inset-0 bg-neutral-100 dark:bg-neutral-800 rounded-full"></div>
<div class="absolute inset-0 bg-emerald-500 rounded-full transition-all duration-300 progress-bar" style="width: ${percentage}%; background-color: ${progressColor};"></div>
</div>
`;
// 插入到旧节点后面
oldSection.after(newElement);
const timeInfoElement = newElement.querySelector('.time-info');
let toggleState = false; // 状态切换:true显示“本月流量统计”,false显示时间范围
// 定时切换显示内容,3秒切换一次
const toggleInterval = setInterval(() => {
if (!document.body.contains(timeInfoElement)) {
clearInterval(toggleInterval);
return;
}
if (toggleState) {
fadeOutIn(timeInfoElement, `<span class="text-[10px] font-medium text-neutral-600 dark:text-neutral-300">本月流量统计</span>`);
} else {
fadeOutIn(timeInfoElement, `
<span class="from-date">${fromFormatted}</span>
<span class="text-neutral-500 dark:text-neutral-400">-</span>
<span class="to-date">${toFormatted}</span>
`);
}
toggleState = !toggleState;
}, 3000);
}
});
}
// 更新流量数据,force 为 true 时强制刷新
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('[updateTrafficStats] 获取失败:', err));
}
// 启动定时刷新
function startPeriodicRefresh() {
if (!trafficTimer) {
trafficTimer = setInterval(() => {
updateTrafficStats();
}, config.interval);
}
}
// 监听 DOM 子节点变化,触发更新
function onDomChildListChange() {
updateTrafficStats();
if (!trafficTimer) {
startPeriodicRefresh();
}
}
// 选择监测的容器节点选择器
const TARGET_SELECTOR = 'section.server-card-list, section.server-inline-list';
let currentSection = null;
let childObserver = null;
// 观察目标区域子节点变化
function observeSection(section) {
if (childObserver) childObserver.disconnect();
currentSection = section;
childObserver = new MutationObserver(mutations => {
for (const m of mutations) {
if (m.type === 'childList' && (m.addedNodes.length || m.removedNodes.length)) {
onDomChildListChange();
break;
}
}
});
childObserver.observe(currentSection, { childList: true, subtree: false });
updateTrafficStats();
}
// 监听页面新增目标容器节点
const sectionDetector = new MutationObserver(() => {
const section = document.querySelector(TARGET_SELECTOR);
if (section && section !== currentSection) {
observeSection(section);
}
});
const root = document.querySelector('main') || document.body;
sectionDetector.observe(root, { childList: true, subtree: true });
startPeriodicRefresh();
// 页面卸载时清理
window.addEventListener('beforeunload', () => {
if (trafficTimer) clearInterval(trafficTimer);
if (childObserver) childObserver.disconnect();
sectionDetector.disconnect();
});
})();
</script>
佬,来个图看看啊
1
bd
6啊
怎么用啊楼主
wc 刚前两天在哪吒群里提这个问题 牛啊哥