logo NodeSeekbeta

哪吒V1探针周期性流量显示分享 下方插入版

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

<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>

你好啊,陌生人!

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

📈用户数目📈

目前论坛共有63702位seeker

🎉欢迎新用户🎉