基于 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 | total > 1e15) { used /= 1e10; total /= 1e10; 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> `

  • ABCDOP
    link
    fedilink
    arrow-up
    1
    ·
    4 days ago

    全部复制 去掉开头符号和结尾的符号