Bangumi Fans Counter Everywhere (班固米谁加我好友人数统计)

个人主页 & 讨论帖显示关注者数量

// ==UserScript==
// @name         Bangumi Fans Counter Everywhere (班固米谁加我好友人数统计)
// @namespace    https://bgm.tv/
// @version      0.2.5
// @description  个人主页 & 讨论帖显示关注者数量
// @match        https://bgm.tv/user/*
// @match        https://bangumi.tv/user/*
// @match        https://bgm.tv/subject/topic/*
// @match        https://bangumi.tv/subject/topic/*
// @match        https://bgm.tv/group/topic/*
// @match        https://bangumi.tv/group/topic/*
// @match        https://bangumi.tv/ep/*
// @match        https://bgm.tv/ep/*
// @match        https://bgm.tv/index/*/comments
// @match        https://bangumi.tv/index/*/comments
// @match        https://bgm.tv/blog/*
// @match        https://bangumi.tv/blog/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @connect      bgm.tv
// @connect      bangumi.tv
// @run-at       document-end
// @license      MIT

// ==/UserScript==

(function () {
  "use strict";

  /* ---------- 配置区 ---------- */
  const CONFIG = {
    // 网络请求与缓存设置
    MAX_CONCURRENT_REQUESTS: 4,
    CACHE_MAX_ITEMS: 1000,
    CACHE_TTL_HOURS: 12,
    // 性能优化设置
    DEBOUNCE_DELAY: 300,
    INTERSECTION_THRESHOLD: 0.1,
    LAZY_LOAD_MARGIN: '200px',

    // V 认证门槛(三档)
    BIG_V_THRESHOLD: 300,
    SUPER_V_THRESHOLD: 600,
    ULTRA_V_THRESHOLD: 1000,

    // 可自定义文本格式
    TEXT_FORMATS: {
      profile: ` 关注者: ${'${cnt}'}人`,
      topic: `关注者:${'${cnt}'}人`,
    },

    // 样式与类名
    BADGE_CLASS: "bgm-fans-count",
    V_BASE_CLASS: "bgm-v",
    BIG_V_CLASS: "bgm-big-v",
    SUPER_V_CLASS: "bgm-super-v",
    ULTRA_V_CLASS: "bgm-ultra-v",
    TOP_FANS_CLASS: "bgm-fans-top",
    TOP_FANS_COLOR: "red",
    LOADING_CLASS: "bgm-fans-loading", // 加载状态类
    LOCATE_BTN_CLASS: "bgm-locate-star-btn",
    HIGHLIGHT_CLASS: "bgm-locate-highlight",

    DEBUG: false,
  };

  /* ---------- 全局样式 ---------- */
  GM_addStyle(`
    /* 关注者数字徽章(胶囊形,统一字体与尺寸) */
    .${CONFIG.BADGE_CLASS} {
      margin-left:4px;
      padding:0 6px;
      height:18px;
      line-height:18px;
      display:inline-flex;
      align-items:center;
      justify-content:center;
      gap:4px;
      font-size:12px;
      font-weight:600;
      color:#222;
      background: linear-gradient(180deg, #ffffff 0%, #f6f7fb 100%);
      border:1px solid rgba(0,0,0,0.08);
      border-radius:10px;
      box-shadow: 0 1px 1px rgba(0,0,0,0.06);
      white-space:nowrap;
      vertical-align: middle;
      font-variant-numeric: tabular-nums;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
    }
    html[data-theme="dark"] .${CONFIG.BADGE_CLASS} {
      color:#f5f5f5;
      background: linear-gradient(180deg, #2a2a2e 0%, #1f2024 100%);
      border-color: rgba(255,255,255,0.12);
      box-shadow: 0 1px 1px rgba(0,0,0,0.4);
    }

    /* 定位按钮(胶囊形) */
    .${CONFIG.LOCATE_BTN_CLASS} {
      display:inline-flex;
      align-items:center;
      justify-content:center;
      height:24px;
      line-height:24px;
      padding:0 12px;
      margin-left:8px;
      border-radius:12px;
      font-size:12px;
      font-weight:700;
      color:#0b57d0;
      background: linear-gradient(180deg, #ffffff 0%, #f3f6ff 100%);
      border:1px solid rgba(11,87,208,0.25);
      box-shadow: 0 1px 1px rgba(0,0,0,0.06);
      cursor:pointer;
      user-select:none;
      transition: background .2s ease, transform .05s ease, box-shadow .2s ease;
    }
    .${CONFIG.LOCATE_BTN_CLASS}:hover { background: linear-gradient(180deg, #ffffff 0%, #eaf0ff 100%); }
    .${CONFIG.LOCATE_BTN_CLASS}:active { transform: translateY(1px); }
    html[data-theme="dark"] .${CONFIG.LOCATE_BTN_CLASS} {
      color:#8ab4ff;
      background: linear-gradient(180deg, #2a2a2e 0%, #1f2024 100%);
      border-color: rgba(138,180,255,0.35);
      box-shadow: 0 1px 1px rgba(0,0,0,0.4);
    }

    /* 定位高亮:脉冲外环 */
    .${CONFIG.HIGHLIGHT_CLASS} {
      position: relative;
      z-index: 1;
    }
    .${CONFIG.HIGHLIGHT_CLASS}::after {
      content: "";
      position: absolute;
      inset: -4px;
      border-radius: 12px;
      pointer-events: none;
      box-shadow: 0 0 0 0 rgba(33,150,243,0.55);
      animation: bgm-pulse-ring 1.2s ease-out 2;
    }
    @keyframes bgm-pulse-ring {
      0% { box-shadow: 0 0 0 0 rgba(33,150,243,0.55); }
      70% { box-shadow: 0 0 0 10px rgba(33,150,243,0); }
      100% { box-shadow: 0 0 0 0 rgba(33,150,243,0); }
    }

    /* 加载状态 */
    .${CONFIG.LOADING_CLASS} {
      margin-left:4px;
      font-size:12px;
      color:#999;
      white-space:nowrap;
      vertical-align: middle;
    }
    .${CONFIG.LOADING_CLASS}::after {
      content: "...";
      animation: loading-dots 1.5s infinite;
    }
    @keyframes loading-dots {
      0%, 20% { content: ""; }
      40% { content: "."; }
      60% { content: ".."; }
      80%, 100% { content: "..."; }
    }

    /* 帖内最高关注者数量高亮 */
    .${CONFIG.TOP_FANS_CLASS} { color:${CONFIG.TOP_FANS_COLOR} !important; }

    /* V 徽标统一基类 */
    .${CONFIG.V_BASE_CLASS} {
      display:inline-flex;
      align-items:center;
      justify-content:center;
      height:16px;
      min-width:16px;
      padding:0 5px;
      margin-left:4px;
      border-radius:8px;
      font-weight:800;
      font-size:10px;
      line-height:1;
      color:#fff !important;
      text-shadow: 0 1px 0 rgba(0,0,0,0.25);
      box-shadow: 0 1px 2px rgba(0,0,0,0.15), inset 0 1px 0 rgba(255,255,255,0.25);
      border:1px solid transparent;
      vertical-align:middle;
      user-select:none;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    }

    /* 300+:橙金渐变 */
    .${CONFIG.BIG_V_CLASS} {
      /* 胶囊形 */
      background: linear-gradient(180deg, #ffcc66 0%, #ffa000 100%);
      border-color: #ffb74d;
      border-radius: 8px;
      padding: 0 6px;
      min-width: 20px;
    }
    html[data-theme="dark"] .${CONFIG.BIG_V_CLASS} {
      background: linear-gradient(180deg, #ffb74d 0%, #ff8f00 100%);
      border-color: #ffa726;
    }

    /* 600+:双配色霓虹六边形 */
    .${CONFIG.SUPER_V_CLASS} {
      position: relative;
      width: 20px;
      height: 18px;
      min-width: 20px;
      padding: 0;
      border-radius: 2px; /* 兜底 */
      /* 双配色渐变:熔岩橙 -> 玫红,比 300+ 更亮眼 */
      background: linear-gradient(135deg, #ff6a00 0%, #ff4d2e 45%, #ff2d8f 100%);
      border-color: transparent;
      /* 六边形外形 */
      clip-path: polygon(25% 0, 75% 0, 100% 50%, 75% 100%, 25% 100%, 0 50%);
      box-shadow:
        0 1px 2px rgba(0,0,0,0.22),
        0 0 0 1px rgba(255, 77, 46, 0.35) inset,
        0 0 10px rgba(255,45,143,0.35);
    }
    /* 内发光与高光描边 */
    .${CONFIG.SUPER_V_CLASS}::after {
      content: '';
      position: absolute;
      inset: 2px;
      border-radius: 2px;
      background: linear-gradient(135deg, rgba(255,255,255,0.22) 0%, rgba(255,255,255,0.06) 60%, rgba(255,255,255,0.0) 100%);
      clip-path: polygon(25% 0, 75% 0, 100% 50%, 75% 100%, 25% 100%, 0 50%);
      box-shadow: inset 0 0 0 1px rgba(255,255,255,0.25);
      pointer-events: none;
    }
    /* 轻微流光效果 */
    .${CONFIG.SUPER_V_CLASS}::before {
      content: '';
      position: absolute;
      inset: -30% -40%;
      background: linear-gradient(120deg, transparent 0%, rgba(255,255,255,0.55) 12%, transparent 24%);
      clip-path: polygon(25% 15%, 75% 15%, 100% 50%, 75% 85%, 25% 85%, 0 50%);
      transform: translateX(-120%);
      animation: bgm-super-shine 2.4s linear infinite;
      pointer-events: none;
    }
    @keyframes bgm-super-shine {
      0% { transform: translateX(-120%); }
      100% { transform: translateX(120%); }
    }
    html[data-theme="dark"] .${CONFIG.SUPER_V_CLASS} {
      background: linear-gradient(135deg, #ff7a1a 0%, #ff4d2e 45%, #ff2d8f 100%);
      box-shadow:
        0 1px 3px rgba(0,0,0,0.45),
        0 0 0 1px rgba(255, 77, 46, 0.45) inset,
        0 0 12px rgba(255,45,143,0.55);
    }

    /* 1000+:尊享金辉 */
    .${CONFIG.ULTRA_V_CLASS} {
      /* 盾牌容器(不直接裁剪本体,避免星芒被裁掉) */
      position: relative;
      padding: 0 8px;
      height: 20px;
      min-width: 24px;
      border-radius: 6px; /* 退化兜底 */
      background: transparent;
      border-color: transparent;
      box-shadow: none;
      z-index: 0;
    }
    /* 盾牌形状背景,置于底层 */
    .${CONFIG.ULTRA_V_CLASS}::after {
      content: "";
      position: absolute;
      inset: 0;
      border-radius: 6px;
      background: linear-gradient(180deg, #ffe082 0%, #ffd54f 45%, #ffc107 100%);
      border: 1px solid #ffd54f;
      box-shadow: 0 1px 2px rgba(0,0,0,0.2), 0 0 8px rgba(255,193,7,0.45), inset 0 1px 0 rgba(255,255,255,0.5);
      clip-path: polygon(12% 0%, 88% 0%, 100% 22%, 100% 66%, 50% 100%, 0% 66%, 0% 22%);
      z-index: -1;
    }
    .${CONFIG.ULTRA_V_CLASS}::before {
      /* 右上角的星芒点缀 */
      content: '★';
      position: absolute;
      right: -4px;
      top: -6px;
      font-size: 9px;
      color:rgb(255, 0, 0);
      text-shadow: 0 0 6px rgba(229, 255, 0, 0.8);
      transform: rotate(-10deg);
      z-index: 1;
      pointer-events: none;
    }
    html[data-theme="dark"] .${CONFIG.ULTRA_V_CLASS}::after {
      background: linear-gradient(180deg, #ffca28 0%, #ffb300 100%);
      border-color: #ffca28;
      box-shadow: 0 1px 3px rgba(0,0,0,0.35), 0 0 10px rgba(255,193,7,0.55), inset 0 1px 0 rgba(255,255,255,0.35);
    }
  `);

  /* ---------- 工具函数 ---------- */
  const parseUsername = (s) => {
    if (!s) return null;
    const match = String(s).match(/^\/?user\/([^/?#]+)/);
    return match ? match[1] : (s.split("/").length >= 3 ? s.split("/")[2] : null);
  };
  const fansUrl = (u) => `${location.origin}/user/${u}/rev_friends`;

  // 仅对含有 memberUserList 的片段进行字符串计数
  const fastCount = (html) => {
    if (!html) return null;
    const start = html.indexOf('id="memberUserList"');
    if (start === -1) return null;
    const end = html.indexOf('</ul>', start);
    const chunk = end === -1 ? html.slice(start) : html.slice(start, end);
    const matches = chunk.match(/class=["']avatar["']/g);
    return matches ? matches.length : 0;
  };

  // 防抖函数
  const debounce = (func, wait) => {
    let timeout;
    return function executedFunction(...args) {
      const later = () => {
        clearTimeout(timeout);
        func(...args);
      };
      clearTimeout(timeout);
      timeout = setTimeout(later, wait);
    };
  };

  // 节流函数
  const throttle = (func, limit) => {
    let inThrottle;
    return function() {
      const args = arguments;
      const context = this;
      if (!inThrottle) {
        func.apply(context, args);
        inThrottle = true;
        setTimeout(() => inThrottle = false, limit);
      }
    }
  };

  const createBadge = (cnt, pageType = 'profile') => {
    const span = document.createElement("span");
    span.className = CONFIG.BADGE_CLASS;
    const fmt = CONFIG.TEXT_FORMATS[pageType] || CONFIG.TEXT_FORMATS.profile;
    span.textContent = fmt.replace('${cnt}', cnt);
    return span;
  };

  const createLoadingBadge = () => {
    const span = document.createElement("span");
    span.className = CONFIG.LOADING_CLASS;
    span.textContent = "加载中";
    return span;
  };

  const createVBadge = (type = "big") => {
    const v = document.createElement("span");
    const specificClass = type === "ultra"
      ? CONFIG.ULTRA_V_CLASS
      : (type === "super" ? CONFIG.SUPER_V_CLASS : CONFIG.BIG_V_CLASS);
    v.className = `${CONFIG.V_BASE_CLASS} ${specificClass}`;
    v.textContent = "V";
    return v;
  };

  /* ---------- 缓存 + 并发队列 ---------- */
  const MAX_CACHE = CONFIG.CACHE_MAX_ITEMS;
  const TTL = CONFIG.CACHE_TTL_HOURS * 60 * 60 * 1e3;
  const MAX_PARALLEL = CONFIG.MAX_CONCURRENT_REQUESTS;

  // 使用 LRU 缓存
  class LRUCache {
    constructor(maxSize) {
      this.maxSize = maxSize;
      this.cache = new Map();
    }

    get(key) {
      if (this.cache.has(key)) {
        const value = this.cache.get(key);
        this.cache.delete(key);
        this.cache.set(key, value);
        return value;
      }
      return null;
    }

    set(key, value) {
      if (this.cache.has(key)) {
        this.cache.delete(key);
      } else if (this.cache.size >= this.maxSize) {
        // 删除最久未使用的项
        const firstKey = this.cache.keys().next().value;
        this.cache.delete(firstKey);
      }
      this.cache.set(key, value);
    }

    has(key) {
      return this.cache.has(key);
    }

    delete(key) {
      return this.cache.delete(key);
    }
  }

  const cache = new LRUCache(MAX_CACHE);
  const pending = new Map();
  const queue = [];
  let working = 0;

  // 持久化缓存(跨会话)
  const persistKey = (u) => `bgm.fans.${u}`;
  const loadPersist = (u) => {
    try { return typeof GM_getValue === 'function' ? GM_getValue(persistKey(u), null) : null; } catch { return null; }
  };
  const savePersist = (u, obj) => {
    try { if (typeof GM_setValue === 'function') GM_setValue(persistKey(u), obj); } catch {}
  };

  const realFetch = (u) => new Promise((resolve) =>
    GM_xmlhttpRequest({
      method: "GET",
      url: fansUrl(u),
      timeout: 10000, // 添加超时
      onload: (r) => {
        let cnt = null;
        if (r.status === 200) {
          try {
            cnt = fastCount(r.responseText);
          } catch (e) {
            console.warn('解析用户关注者数据失败:', e);
          }
        }
        const result = { cnt: (typeof cnt === 'number' ? cnt : "未知"), ts: Date.now() };
        cache.set(u, result);
        if (typeof cnt === 'number') savePersist(u, result);
        resolve(cnt);
      },
      onerror: () => resolve(null),
      ontimeout: () => resolve(null),
    })
  );

  const dequeue = () => {
    while (working < MAX_PARALLEL && queue.length) {
      const { u, ok } = queue.shift();
      working++;
      realFetch(u).then((c) => {
        working--;
        ok(c);
        dequeue();
      });
    }
  };

  function getFansCount(u) {
    if (cache.has(u)) {
      const o = cache.get(u);
      if (Date.now() - o.ts < TTL) return Promise.resolve(o.cnt);
      cache.delete(u);
    }
    const persisted = loadPersist(u);
    if (persisted && Date.now() - persisted.ts < TTL) {
      cache.set(u, persisted);
      return Promise.resolve(persisted.cnt);
    }
    if (pending.has(u)) return pending.get(u);
    const p = new Promise((ok) => { queue.push({ u, ok }); dequeue(); });
    pending.set(u, p);
    p.finally(() => pending.delete(u));
    return p;
  }

  /* ---------- 最高关注者数量跟踪 ---------- */
  let topicMax = -1;
  let maxBadges = [];
  function updateMax(badge, cnt) {
    if (cnt == null || cnt === "未知") return;
    if (cnt > topicMax) {
      maxBadges.forEach((b) => b.classList.remove(CONFIG.TOP_FANS_CLASS));
      topicMax = cnt;
      maxBadges = [badge];
      badge.classList.add(CONFIG.TOP_FANS_CLASS);
    } else if (cnt === topicMax) {
      maxBadges.push(badge);
      badge.classList.add(CONFIG.TOP_FANS_CLASS);
    }
  }

  /* ---------- 非用户主页统计 开关(默认开) ---------- */
  const NONUSER_TOGGLE_KEY = 'bgm.fans.enableNonUserPages';
  let enableNonUserPages = true;
  let toggleButtonEl = null;

  const loadNonUserToggle = () => {
    try {
      return typeof GM_getValue === 'function' ? GM_getValue(NONUSER_TOGGLE_KEY, true) : true;
    } catch {
      return true;
    }
  };
  const saveNonUserToggle = (val) => {
    try { if (typeof GM_setValue === 'function') GM_setValue(NONUSER_TOGGLE_KEY, val); } catch {}
  };
  enableNonUserPages = loadNonUserToggle();

  const updateToggleButtonLabel = () => {
    if (!toggleButtonEl) return;
    toggleButtonEl.textContent = `非主页统计: ${enableNonUserPages ? '开' : '关'}`;
  };

  const cleanupTopicDOM = (root = document) => {
    // 移除已插入的徽章/加载态,移除高亮
    const selector = [
      `.${CONFIG.BADGE_CLASS}`,
      `.${CONFIG.V_BASE_CLASS}`,
      `.${CONFIG.BIG_V_CLASS}`,
      `.${CONFIG.SUPER_V_CLASS}`,
      `.${CONFIG.ULTRA_V_CLASS}`,
      `.${CONFIG.LOADING_CLASS}`,
      `.${CONFIG.HIGHLIGHT_CLASS}`
    ].join(',');
    root.querySelectorAll(selector).forEach((el) => {
      if (el.classList && el.classList.contains(CONFIG.HIGHLIGHT_CLASS)) {
        el.classList.remove(CONFIG.HIGHLIGHT_CLASS);
      } else {
        el.remove();
      }
    });
    // 重置与停止观察
    topicMax = -1;
    maxBadges = [];
    pendingUpdates.clear();
    clearUpdateHandle();
    if (mutationObserver) mutationObserver.disconnect();
    if (intersectionObserver) { intersectionObserver.disconnect(); intersectionObserver = null; }
  };

  /* ---------- 在页头插入“定位bangumi大明星”按钮 ---------- */
  function insertLocateButton() {
    const header = document.querySelector('h2.reply_title span.reply_author');
    if (!header) return;
    // 防止重复插入
    if (header.nextElementSibling && header.nextElementSibling.classList && header.nextElementSibling.classList.contains(CONFIG.LOCATE_BTN_CLASS)) return;

    const btn = document.createElement('button');
    btn.type = 'button';
    btn.className = CONFIG.LOCATE_BTN_CLASS;
    btn.textContent = '定位bangumi大明星';

    btn.addEventListener('click', () => {
      // 若还未计算出最大值,提醒用户稍等
      if (topicMax < 0 || maxBadges.length === 0) {
        // 触发一次增强,尽快补全数据
        enhanceTopic();
        observeTopic();
        btn.disabled = true;
        setTimeout(() => { btn.disabled = false; }, 600);
        return;
      }

      // 取消之前的高亮
      document.querySelectorAll('.' + CONFIG.HIGHLIGHT_CLASS).forEach(el => el.classList.remove(CONFIG.HIGHLIGHT_CLASS));

      // 高亮所有并滚动到第一个
      const first = maxBadges[0];
      maxBadges.forEach(b => b.classList.add(CONFIG.HIGHLIGHT_CLASS));
      if (first && typeof first.scrollIntoView === 'function') {
        first.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }
    });

    header.after(btn);

    // 在定位按钮之后插入“非用户页统计”开关按钮
    if (!document.getElementById('bgm-toggle-nonuser')) {
      const tbtn = document.createElement('button');
      tbtn.type = 'button';
      tbtn.id = 'bgm-toggle-nonuser';
      tbtn.className = CONFIG.LOCATE_BTN_CLASS;
      tbtn.title = '切换是否在非用户页显示与计算关注数(个人主页始终显示)';
      toggleButtonEl = tbtn;
      updateToggleButtonLabel();
      tbtn.addEventListener('click', () => {
        enableNonUserPages = !enableNonUserPages;
        saveNonUserToggle(enableNonUserPages);
        updateToggleButtonLabel();
        if (enableNonUserPages) {
          topicMax = -1;
          maxBadges = [];
          enhanceTopic();
          observeTopic();
        } else {
          cleanupTopicDOM(document);
        }
      });
      btn.after(tbtn);
    }
  }

  /* ---------- 批量DOM更新 ---------- */
  const pendingUpdates = new Map();
  let updateTimeout = null;
  const updateUseIdle = typeof requestIdleCallback === 'function';
  const clearUpdateHandle = () => {
    if (!updateTimeout) return;
    if (updateUseIdle && typeof cancelIdleCallback === 'function') {
      cancelIdleCallback(updateTimeout);
    } else {
      clearTimeout(updateTimeout);
    }
    updateTimeout = null;
  };

  const batchUpdateDOM = () => {
    clearUpdateHandle();
    const run = () => {
      const updates = Array.from(pendingUpdates.entries());
      updates.forEach(([element, data]) => {
        updateElementWithFansData(element, data.count, data.pageType);
      });
      pendingUpdates.clear();
      updateTimeout = null;
    };
    updateTimeout = updateUseIdle
      ? requestIdleCallback(run, { timeout: 200 })
      : setTimeout(run, 50);
  };

  const updateElementWithFansData = (element, count, pageType) => {
    // 清理旧的徽章
    let next = element.nextElementSibling;
    while (next && (
      next.classList.contains(CONFIG.BADGE_CLASS) ||
      next.classList.contains(CONFIG.V_BASE_CLASS) ||
      next.classList.contains(CONFIG.BIG_V_CLASS) ||
      next.classList.contains(CONFIG.SUPER_V_CLASS) ||
      next.classList.contains(CONFIG.ULTRA_V_CLASS) ||
      next.classList.contains(CONFIG.LOADING_CLASS)
    )) {
      const toRemove = next;
      next = next.nextElementSibling;
      toRemove.remove();
    }

    if (count == null) return;

    // 一次性插入,避免多次重排并保证顺序:徽章在前,V 在后
    const nodesToInsert = [];
    const fanBadge = createBadge(count, pageType);
    nodesToInsert.push(fanBadge);
    if (count >= CONFIG.ULTRA_V_THRESHOLD) {
      nodesToInsert.push(createVBadge('ultra'));
    } else if (count >= CONFIG.SUPER_V_THRESHOLD) {
      nodesToInsert.push(createVBadge('super'));
    } else if (count >= CONFIG.BIG_V_THRESHOLD) {
      nodesToInsert.push(createVBadge('big'));
    }
    element.after(...nodesToInsert);

    if (pageType === 'topic') {
      updateMax(fanBadge, count);
    }
  };

  /* ---------- 可见性检测 ---------- */
  let intersectionObserver = null;

  const createIntersectionObserver = () => {
    if (intersectionObserver) return intersectionObserver;

    intersectionObserver = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const element = entry.target;
          if (element.dataset.needsProcessing === "true") {
            processUserLink(element);
            element.dataset.needsProcessing = "false";
            intersectionObserver.unobserve(element);
          }
        }
      });
    }, {
      rootMargin: CONFIG.LAZY_LOAD_MARGIN,
      threshold: CONFIG.INTERSECTION_THRESHOLD
    });

    return intersectionObserver;
  };

  let processUserLink = (element) => {
    if (element.dataset.fetched) return;
    element.dataset.fetched = "1";

    const u = parseUsername(element.getAttribute("href"));
    if (!u) return;

    if (!enableNonUserPages) return;

    // 添加加载状态
    const loadingBadge = createLoadingBadge();
    element.after(loadingBadge);

    getFansCount(u).then((c) => {
      // 移除加载状态
      if (loadingBadge.parentNode) {
        loadingBadge.remove();
      }

      if (c != null) {
        pendingUpdates.set(element, { count: c, pageType: 'topic' });
        batchUpdateDOM();
      }
    });
  };

  /* ---------- 个人主页 ---------- */
  function enhanceProfile() {
    const u = parseUsername(location.pathname);
    if (!u) return;
    const anchor = document.querySelector("h1.nameSingle small.grey");
    if (!anchor) return;

    getFansCount(u).then((c) => {
      if (c == null) return;

      // 清理旧徽章
      while (anchor.nextElementSibling &&
            (anchor.nextElementSibling.classList.contains(CONFIG.BADGE_CLASS) ||
             anchor.nextElementSibling.classList.contains(CONFIG.V_BASE_CLASS) ||
             anchor.nextElementSibling.classList.contains(CONFIG.BIG_V_CLASS) ||
             anchor.nextElementSibling.classList.contains(CONFIG.SUPER_V_CLASS) ||
             anchor.nextElementSibling.classList.contains(CONFIG.ULTRA_V_CLASS))) {
        anchor.nextElementSibling.remove();
      }

      const nodesToInsert = [];
      const fanBadge = createBadge(c, 'profile');
      nodesToInsert.push(fanBadge);
      if (c >= CONFIG.ULTRA_V_THRESHOLD) {
        nodesToInsert.push(createVBadge('ultra'));
      } else if (c >= CONFIG.SUPER_V_THRESHOLD) {
        nodesToInsert.push(createVBadge('super'));
      } else if (c >= CONFIG.BIG_V_THRESHOLD) {
        nodesToInsert.push(createVBadge('big'));
      }
      anchor.after(...nodesToInsert);
    });
  }

  /* ---------- 讨论 / 小组帖 / 章节 ---------- */
  function enhanceTopic(root = document) {
    // 非用户页统计被关闭时,仅插入按钮,不进行统计
    if (!enableNonUserPages) {
      insertLocateButton();
      return;
    }
    const allUserLinks = root.querySelectorAll('a[href^="/user/"]');
    const links = Array.from(allUserLinks).filter((a) =>
      !a.closest('.likes_grid, .tooltip, .tags, .reply_title') &&
      !a.classList.contains('avatar') &&
      !a.classList.contains('tip_i') &&
      !(a.getAttribute('style') || '').includes('background') &&
      !a.dataset.fetched
    );

    if (links.length === 0) {
      insertLocateButton();
      return;
    }

    const observer = createIntersectionObserver();

    // 统一交给 IntersectionObserver
    links.forEach((link) => {
      link.dataset.needsProcessing = "true";
      observer.observe(link);
    });

    // 每次增强时确保按钮存在
    insertLocateButton();
  }

  /* ---------- DOM监听 ---------- */
  let mutationObserver = null;
  const debouncedEnhanceTopic = debounce((root) => {
    enhanceTopic(root);
  }, CONFIG.DEBOUNCE_DELAY);

  function observeTopic() {
    if (!enableNonUserPages) return;
    const node = document.querySelector("#comment_list") || document.querySelector("#entry_content");
    if (!node) return;

    if (mutationObserver) {
      mutationObserver.disconnect();
    }

    mutationObserver = new MutationObserver(throttle((mutations) => {
      let hasNewNodes = false;

      mutations.forEach((mutation) => {
        if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
          mutation.addedNodes.forEach((node) => {
            if (node.nodeType === Node.ELEMENT_NODE) {
              hasNewNodes = true;
            }
          });
        }
      });

      if (hasNewNodes) {
        debouncedEnhanceTopic(node);
      }
    }, 100)); // 100ms 节流

    mutationObserver.observe(node, {
      childList: true,
      subtree: true
    });
  }

  /* ---------- 页面卸载清理 ---------- */
  window.addEventListener('beforeunload', () => {
    if (mutationObserver) mutationObserver.disconnect();
    if (intersectionObserver) intersectionObserver.disconnect();
    if (updateTimeout) clearTimeout(updateTimeout);
  });

  /* ---------- 启动 ---------- */
  const p = location.pathname;
  if (/^\/user\/[^/]+/.test(p)) {
    enhanceProfile();
  } else if (/\/(subject|group)\/topic\//.test(p) || /^\/ep\//.test(p) || /^\/index\/.+\/comments$/.test(p) || /^\/blog\//.test(p)) {
    // 延迟初始化,避免阻塞页面加载
    setTimeout(() => {
      enhanceTopic();
      if (enableNonUserPages) observeTopic();
    }, 100);
  }

  // 性能监控(调试用)
  if (CONFIG.DEBUG) {
    let processedCount = 0;
    const originalProcessUserLink = processUserLink;
    processUserLink = function(element) {
      processedCount++;
      console.log(`已处理用户链接数量: ${processedCount}`);
      return originalProcessUserLink.call(this, element);
    };
  }
})();