您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
个人主页 & 讨论帖显示关注者数量
// ==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); }; } })();