logo NodeSeekbeta

哪吒V1探针周期性流量显示分享

预览:
image
基于https://www.nodeseek.com/post-313735-1 帖子的nezha美化修改
基于卡片里上传下载(window.ShowNetTransfer = "true";) 修改了流量条展示位置,可调整 insertPosition: 'before', // 可选值:'after', 'before', 'replace'
如没打开卡片显示上下行流量,默认在详情后面展示
修改了渲染方式,在页面结构变化后首次0ms刷新,之后60s周期性刷新

<style>
  /* 不显示自带流量卡片 */
  .mt-4.w-full.mx-auto > div {
    display: none;
  }
</style>

<script>
  /* 卡片显示上下行流量 */
  //window.ShowNetTransfer = "true";
</script>

<script>
  ; (function () {
    let trafficTimer = null;
    let trafficCache = null;

    const config = {
      showTrafficStats: true,
      insertPosition: 'replace', // 可选:'after', 'before', 'replace'
      interval: 60000,           // 60秒刷新周期
      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;
      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 (used / total * 100).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 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 }; // #10b981
      const endColor = { r: 239, g: 68, b: 68 };  // #ef4444
      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})`;
    }

    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) {
          //console.warn(`[renderTrafficStats] 未找到服务器 "${serverName}" (ID: ${serverData.id}) 的元素`);
          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);
        let insertPosition = config.insertPosition;

        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, '.from-date', fromFormatted);
          safeSetTextContent(existing, '.to-date', toFormatted);
          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;
          }
          console.log(`[renderTrafficStats] 更新已存在的流量条目: ${serverName}`);
        }
        else {
          let oldSection = containerDiv.querySelector('section.flex.items-center.w-full.justify-between.gap-1');
          if (!oldSection) {
            oldSection = containerDiv.querySelector('section.grid.items-center.gap-3');
            insertPosition = 'after';
          }
          if (!oldSection) return;

          const newElement = document.createElement('div');
          newElement.classList.add('space-y-1.5', 'new-inserted-element', uniqueClassName);
          newElement.style.width = '100%';
          if (config.style === 1) {
            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">
                  <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>
              `;
          } else if (config.style === 2) {
            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>
                  <span class="text-[10px] text-neutral-500 dark:text-neutral-400 percentage-value">${percentage}%</span>
                </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>
                <div class="flex items-center justify-between">
                  <div class="text-[10px] text-neutral-500 dark:text-neutral-400">
                    <span class="from-date">${fromFormatted}</span>
                    <span class="text-neutral-500 dark:text-neutral-400">-</span>
                    <span class="to-date">${toFormatted}</span>
                  </div>
                  <span class="text-[10px] text-neutral-500 dark:text-neutral-400">next update: ${next_update}</span>
                </div>
              `;
          }
          if (insertPosition === 'before') oldSection.before(newElement);
          else if (insertPosition === 'replace') oldSection.replaceWith(newElement);
          else oldSection.after(newElement);
          console.log(`[renderTrafficStats] 插入新流量条目: ${serverName},插入方式: ${insertPosition}`);
        }
      });
    }

    function updateTrafficStats(force = false) {
      const now = Date.now();
      if (!force && trafficCache && (now - trafficCache.timestamp < config.interval)) {
        console.log('[updateTrafficStats] 使用缓存数据');
        renderTrafficStats(trafficCache.data);
        return;
      }

      console.log('[updateTrafficStats] 正在请求新数据...');
      fetch('/api/v1/service')
        .then(res => res.json())
        .then(data => {
          if (!data.success) {
            console.warn('[updateTrafficStats] 请求成功但返回数据异常');
            return;
          }
          console.log('[updateTrafficStats] 成功获取新数据');
          const trafficData = data.data.cycle_transfer_stats;
          trafficCache = {
            timestamp: now,
            data: trafficData
          };
          renderTrafficStats(trafficData);
        })
        .catch(err => console.error('[updateTrafficStats] 获取失败:', err));
    }

    function startPeriodicRefresh() {
      if (!trafficTimer) {
        console.log('[startPeriodicRefresh] 启动周期刷新任务');
        trafficTimer = setInterval(() => {
          updateTrafficStats();
        }, config.interval);
      }
    }

    function onDomChildListChange() {
      console.log('[onDomChildListChange] 检测到DOM变化, 立即刷新');
      updateTrafficStats();
      if (!trafficTimer) {
        console.log('[onDomChildListChange] 启动定时刷新');
        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();
        console.log('[监听] 断开旧的子节点监听');
      }

      currentSection = section;
      console.log('[监听] 监听新的目标 section 子节点');

      childObserver = new MutationObserver(mutations => {
        for (const m of mutations) {
          if (m.type === 'childList' && (m.addedNodes.length || m.removedNodes.length)) {
            console.log('[监听] section 子节点变化,触发刷新');
            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 });
    console.log('[初始化] 启动 sectionDetector, 持续监听 section 切换');

    startPeriodicRefresh();

    window.addEventListener('beforeunload', () => {
      if (trafficTimer) clearInterval(trafficTimer);
      if (childObserver) childObserver.disconnect();
      sectionDetector.disconnect();
      console.log('[清理] 卸载监听器和定时器');
    });
  })();
</script>

update: 添加了缓存加快渲染速度
update: 添加横向流量条展示
{B0F10F94-51E9-4D64-830F-0AE2C7B3741B}
update: 添加不同进度条风格

    const config = {
      showTrafficStats: true,
      insertPosition: 'replace', // 可选:'after', 'before', 'replace'
      interval: 60000,           // 60秒刷新周期
      style: 1
    };

{BB018059-7355-4EC3-A77E-9C248B1F0421}
{B0F10F94-51E9-4D64-830F-0AE2C7B3741B}

    const config = {
      showTrafficStats: true,
      insertPosition: 'replace', // 可选:'after', 'before', 'replace'
      interval: 60000,           // 60秒刷新周期
      style: 2
    };

{B9117F8A-67C0-4D48-9FB0-64145229244B}
{701AFB6C-6A87-46EE-83EE-3147FBE4C70C}
update: 优化sectionDetector子节点监听,在切换分组等情况后仍然渲染
update: 参考 https://www.nodeseek.com/post-355300-1 添加进度条颜色
update: 精准匹配existing更新进度条,精准匹配服务器名, 修复出现模糊匹配的问题

ps:
周期性流量需要针对每台机器配置对应统计周期,统计方式,流量总量信息,可以参考https://nezha.wiki/guide/q6.html 配置, 使用 https://nzcfg.pages.dev/ 进行配置告警信息

  • <script>
      /* 卡片显示上下行流量 */
      window.ShowNetTransfer = "true";
    </script>
    

    没生效的要添加这个,因为是基于这个为锚点进行修改的

  • 新美化!好!
    bd

  • bd

  • xhj016

  • 点赞,马克

  • xhj003

  • mark

  • 点赞

  • 这个不错,mark了

  • 为什么我的没效果

  • 这个不错,mark了

你好啊,陌生人!

我的朋友,看起来你是新来的,如果想参与到讨论中,点击下面的按钮!

📈用户数目📈

目前论坛共有63695位seeker

🎉欢迎新用户🎉