// ==UserScript== // @name B站关注管理 // @namespace http://tampermonkey.net/ // @version 1.0 // @description 高效管理B站关注列表,支持批量取关、智能筛选等功能。支持智能筛选、实时粉丝数获取、批量操作等功能 // @author 苡淞(Yis_Rime) // @homepage https://github.com/YisRime // @match https://space.bilibili.com/*/relation/follow* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @connect api.bilibili.com // @license AGPLv3.0 // @downloadURL none // ==/UserScript== (function() { 'use strict'; // 配置常量 const CONFIG = { CACHE_DURATION: 24 * 60 * 60 * 1000, API_DELAY: 500, // 进一步增加API延迟 RETRY_DELAY: 1000, // 增加重试延迟 MAX_RETRIES: 2, // 减少重试次数避免卡死 PAGE_SIZE: 100, // 减少页面大小 SCROLL_DEBOUNCE: 200, // 增加滚动防抖时间 FILTER_DEBOUNCE: 250, // 增加筛选防抖时间 BATCH_DELAY: 200, // 增加批量操作延迟 PARALLEL_FANS_COUNT: 1, FANS_BATCH_SIZE: 1, FANS_API_DELAY: 500, // 大幅增加粉丝数API延迟 FANS_RETRY_DELAY: 1000, // 大幅增加重试延迟 MAX_CONCURRENT_REQUESTS: 1, // 限制并发请求数 REQUEST_TIMEOUT: 10000, // 请求超时时间 MAX_RENDER_ITEMS: 5000, // 最大渲染项目数 VIRTUAL_SCROLL_THRESHOLD: 200 // 虚拟滚动阈值 }; // 统一文字提示 const MESSAGES = { LOADING: '正在加载数据...', LOADING_PAGE: '正在加载第{0}页...', RETRY: '加载失败,正在重试...', ERROR_PERMISSION: '权限不足或用户隐私设置不允许访问', ERROR_API: 'API错误: {0}', ERROR_NETWORK: '网络请求失败', ERROR_NO_DATA: '未能获取关注列表,请检查是否已登录', ERROR_NO_UID: '无法获取用户UID,请确保已登录', CONFIRM_UNFOLLOW: '确定要取关以下 {0} 个用户吗?\n\n{1}\n\n此操作不可撤销!', CONFIRM_FETCH_ALL: '确定要获取所有用户的粉丝数吗?这可能需要一些时间。', CONFIRM_CLEAR_CACHE: '确定要清除缓存吗?下次打开将重新加载关注列表。', SUCCESS_CACHE_CLEARED: '缓存已清除!', SUCCESS_UNFOLLOW: '批量取关完成!\n成功: {0}个,失败: {1}个', SUCCESS_FETCH_FANS: '粉丝数获取完成!\n成功处理: {0}/{1} 个用户', PROGRESS_UNFOLLOW: '取关进度: {0}/{1} (成功{2}个)', PROGRESS_FETCH: '获取中... {0}/{1} ({2}%)', SELECT_USERS: '请选择要取关的用户' };// 添加样式 GM_addStyle(` .follow-manager-btn { position: fixed; top: 100px; right: 30px; z-index: 9999; background: #00a1d6; color: #fff; padding: 10px 20px; border-radius: 6px; cursor: pointer; border: none; box-shadow: 0 4px 12px rgba(0,161,214,0.3); transition: all 0.3s; font-size: 14px; font-weight: 500; } .follow-manager-btn:hover { background: #0088cc; transform: translateY(-2px); box-shadow: 0 6px 16px rgba(0,161,214,0.4); } .follow-manager-btn:disabled { background: #ccc; cursor: not-allowed; transform: none; } .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 10000; display: flex; justify-content: center; align-items: center; backdrop-filter: blur(4px); } .modal-content { background: #fff; border-radius: 12px; width: 95vw; max-width: 1600px; height: 90vh; max-height: 900px; overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 20px 40px rgba(0,0,0,0.15); } .modal-header { padding: 20px 25px; border-bottom: 1px solid #e9ecef; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; display: flex; align-items: center; } .modal-header h3 { margin: 0; font-size: 20px; font-weight: 600; } .close-btn { background: none; border: none; font-size: 24px; cursor: pointer; color: #fff; transition: color 0.2s; margin-left: auto; } .close-btn:hover { color: #ff4757; } .modal-body { flex: 1; overflow: hidden; display: flex; flex-direction: column; padding: 20px; } .modal-filters { background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 15px; border: 1px solid #dee2e6; } .filter-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } .filter-row input, .filter-row select { padding: 8px 12px; border: 1px solid #ced4da; border-radius: 6px; font-size: 13px; background: #fff; transition: all 0.2s; } .filter-row input:focus, .filter-row select:focus { outline: none; border-color: #007bff; box-shadow: 0 0 0 2px rgba(0,123,255,0.25); } .filter-row input[type="text"] { flex: 1; min-width: 300px; } .filter-row input[type="date"] { min-width: 120px; } .filter-row select { min-width: 100px; } .filter-row label { font-size: 13px; white-space: nowrap; color: #495057; font-weight: 500; } .modal-list { flex: 1; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 8px; background: #fff; } .modal-stats { padding: 15px 20px; background: #f8f9fa; border-top: 1px solid #dee2e6; display: flex; justify-content: space-between; align-items: center; font-weight: 500; } .follow-table { width: 100%; border-collapse: collapse; font-size: 14px; } .follow-table th { background: #fff; padding: 12px 8px; border-bottom: 2px solid #dee2e6; text-align: left; font-weight: 600; position: sticky; top: 0; z-index: 2; cursor: pointer; user-select: none; transition: all 0.2s; white-space: nowrap; } .follow-table th:hover { background: #f8f9fa; } .follow-table th.sortable::after { content: '↕'; margin-left: 4px; color: #6c757d; font-size: 11px; } .follow-table th.sort-asc::after { content: '↑'; color: #007bff; } .follow-table th.sort-desc::after { content: '↓'; color: #007bff; } .follow-table td { padding: 10px 8px; border-bottom: 1px solid #f1f3f4; vertical-align: middle; word-wrap: break-word; } .follow-table tr:hover { background: #f8f9fa; } .follow-table tr.selected { background: #e3f2fd; } .follow-table img { width: 40px; height: 40px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 2px 8px rgba(0,0,0,0.15); } .batch-actions { display: flex; gap: 10px; flex-wrap: wrap; } .batch-actions button { padding: 8px 16px; border: 1px solid #007bff; border-radius: 4px; background: #007bff; color: white; cursor: pointer; transition: all 0.2s; font-size: 13px; } .batch-actions button:hover { background: #0056b3; } .batch-actions button:disabled { background: #6c757d; border-color: #6c757d; cursor: not-allowed; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .loading-spinner { display: inline-block; width: 20px; height: 20px; border: 2px solid #007bff; border-top: 2px solid transparent; border-radius: 50%; animation: spin 1s linear infinite; margin-right: 10px; } .follow-table th:nth-child(1) { width: 40px; } .follow-table th:nth-child(2) { width: 50px; } .follow-table th:nth-child(3) { width: 80px; max-width: 80px; } .follow-table th:nth-child(4) { width: 80px; } .follow-table th:nth-child(5) { width: 100px; } .follow-table th:nth-child(6) { width: 600px; max-width: 600px; } .follow-table th:nth-child(7) { width: 120px; } .follow-table td:nth-child(3), .follow-table td:nth-child(6) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .modal-list::-webkit-scrollbar { width: 18px; } .modal-list::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 9px; } .modal-list::-webkit-scrollbar-thumb { background: #888; border-radius: 9px; min-height: 30px; } .modal-list::-webkit-scrollbar-thumb:hover { background: #555; } `); // 缓存管理 const cache = { memoryCache: new Map(), // 内存缓存 filterCache: new Map(), // 筛选结果缓存 debounceTimers: new Map(), // 防抖定时器 get() { try { const cached = localStorage.getItem('bilibili_follow_cache'); if (cached) { const data = JSON.parse(cached); if (data.expiry > Date.now()) { this.memoryCache.set('followList', data.list); return data.list; } } } catch (e) { console.warn('读取缓存失败:', e); } return null; }, set(list) { try { this.memoryCache.set('followList', list); localStorage.setItem('bilibili_follow_cache', JSON.stringify({ list, expiry: Date.now() + CONFIG.CACHE_DURATION })); } catch (e) { console.warn('保存缓存失败:', e); } }, getUserInfo(mid) { return this.memoryCache.get(`user_${mid}`); }, setUserInfo(mid, info) { this.memoryCache.set(`user_${mid}`, info); }, // 从本地存储获取用户信息 getUserInfoFromLocal(mid) { try { const cached = localStorage.getItem(`bilibili_user_${mid}`); if (cached) { const data = JSON.parse(cached); // 检查缓存是否过期(24小时) if (data.timestamp && Date.now() - data.timestamp < CONFIG.CACHE_DURATION) { return data; } } } catch (e) { console.warn(`读取用户${mid}本地缓存失败:`, e); } return null; }, // 保存用户信息到本地存储 setUserInfoToLocal(mid, info) { try { localStorage.setItem(`bilibili_user_${mid}`, JSON.stringify(info)); } catch (e) { console.warn(`保存用户${mid}本地缓存失败:`, e); } }, getFilterResult(key) { return this.filterCache.get(key); }, setFilterResult(key, result) { if (this.filterCache.size > 10) { const firstKey = this.filterCache.keys().next().value; this.filterCache.delete(firstKey); } this.filterCache.set(key, result); }, debounce(key, fn, delay) { if (this.debounceTimers.has(key)) { clearTimeout(this.debounceTimers.get(key)); } const timer = setTimeout(() => { fn(); this.debounceTimers.delete(key); }, delay); this.debounceTimers.set(key, timer); } }; // 内存管理和错误恢复 const memoryManager = { // 清理内存缓存 cleanMemoryCache() { if (cache.memoryCache.size > 1000) { const entries = Array.from(cache.memoryCache.entries()); // 保留最近使用的500个 const recentEntries = entries.slice(-500); cache.memoryCache.clear(); recentEntries.forEach(([key, value]) => { cache.memoryCache.set(key, value); }); } }, // 检查页面响应性 checkPageResponsiveness() { let lastTime = performance.now(); return new Promise(resolve => { requestAnimationFrame(() => { const currentTime = performance.now(); const lagTime = currentTime - lastTime; resolve(lagTime < 50); // 如果延迟超过50ms认为页面卡顿 }); }); }, // 强制垃圾回收提示 triggerGC() { if (window.gc && typeof window.gc === 'function') { try { window.gc(); } catch (e) { // 忽略错误 } } } }; // 操作监控器 - 避免长时间运行操作卡死页面 const operationMonitor = { activeOperations: new Map(), // 注册操作 register(id, operation) { this.activeOperations.set(id, { operation, startTime: Date.now(), lastUpdate: Date.now() }); }, // 更新操作时间戳 update(id) { const op = this.activeOperations.get(id); if (op) { op.lastUpdate = Date.now(); } }, // 注销操作 unregister(id) { this.activeOperations.delete(id); }, // 检查是否有长时间运行的操作 checkLongRunningOperations() { const now = Date.now(); const maxDuration = 60000; // 60秒 for (const [id, op] of this.activeOperations) { if (now - op.startTime > maxDuration) { console.warn(`操作 ${id} 已运行超过60秒,可能出现卡死`); // 可以在这里添加自动取消逻辑 } } } }; // 定期检查长时间运行的操作 setInterval(() => { operationMonitor.checkLongRunningOperations(); }, 30000); // 每30秒检查一次 // 页面卸载时清理资源 window.addEventListener('beforeunload', () => { // 清理定时器 cache.debounceTimers.forEach(timer => clearTimeout(timer)); cache.debounceTimers.clear(); // 清理内存缓存 memoryManager.cleanMemoryCache(); }); // 定期清理内存 setInterval(() => { memoryManager.cleanMemoryCache(); memoryManager.triggerGC(); }, 60000); // 每分钟清理一次 // 工具函数 const formatMessage = (template, ...args) => { return template.replace(/\{(\d+)\}/g, (match, index) => args[index] || match); }; const getUserId = () => { const uidMatch = location.pathname.match(/space\/(\d+)/); if (uidMatch) return uidMatch[1]; const cookieMatch = document.cookie.match(/DedeUserID=(\d+)/); if (cookieMatch) return cookieMatch[1]; try { const linkElement = document.querySelector('.h-info a[href*="/space/"]'); if (linkElement) { const hrefMatch = linkElement.href.match(/space\/(\d+)/); if (hrefMatch) return hrefMatch[1]; } if (window.__INITIAL_STATE__ && window.__INITIAL_STATE__.mid) return window.__INITIAL_STATE__.mid; if (window.BilibiliPlayer && window.BilibiliPlayer.mid) return window.BilibiliPlayer.mid; } catch (e) {} return null; }; const getCsrfToken = () => document.cookie.match(/bili_jct=([^;]+)/)?.[1] || ''; // 获取用户粉丝数信息 const fetchUserInfo = async (mid) => { // 检查内存缓存 const cached = cache.getUserInfo(mid); if (cached) return cached; // 检查本地存储缓存 const localCached = cache.getUserInfoFromLocal(mid); if (localCached) { cache.setUserInfo(mid, localCached); return localCached; } // 添加请求控制器来处理超时 const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), CONFIG.REQUEST_TIMEOUT); try { const res = await fetch(`https://api.bilibili.com/x/relation/stat?vmid=${mid}`, { credentials: 'include', headers: { 'Referer': 'https://space.bilibili.com/' }, signal: controller.signal }); clearTimeout(timeoutId); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); if (data.code === 0 && data.data) { const info = { follower: data.data.follower || 0, following: data.data.following || 0, timestamp: Date.now() }; cache.setUserInfo(mid, info); cache.setUserInfoToLocal(mid, info); return info; } else if (data.code === -404 || data.code === -400) { const info = { follower: -1, following: -1, timestamp: Date.now() }; cache.setUserInfo(mid, info); cache.setUserInfoToLocal(mid, info); return info; } } catch (error) { clearTimeout(timeoutId); // 静默处理错误,避免卡死 console.warn(`获取用户${mid}信息失败:`, error.message); } // 默认返回0并缓存 const defaultInfo = { follower: 0, following: 0, timestamp: Date.now() }; cache.setUserInfo(mid, defaultInfo); cache.setUserInfoToLocal(mid, defaultInfo); return defaultInfo; }; // 取消关注函数 const unfollowUser = async (mid) => { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: 'https://api.bilibili.com/x/relation/modify', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Cookie': document.cookie }, data: `fid=${mid}&act=2&re_src=11&csrf=${getCsrfToken()}`, onload: function(response) { try { const data = JSON.parse(response.responseText); if (data.code === 0) { resolve(data); } else { reject(new Error(data.message || '取关失败')); } } catch (e) { reject(new Error('响应解析失败')); } }, onerror: function(error) { reject(new Error('网络请求失败')); } }); }); }; class FollowManager { constructor() { this.modal = null; this.isLoading = false; this.isOperationCancelled = false; this.createButton(); } createButton() { const btn = document.createElement('button'); btn.innerText = '管理关注'; btn.className = 'follow-manager-btn'; btn.onclick = () => this.toggleModal(); document.body.appendChild(btn); this.btn = btn; } async toggleModal() { if (this.modal) return this.closeModal(); if (this.isLoading) return; this.setLoading(true); try { this.modal = new FollowModal([], () => this.closeModal()); const cachedList = cache.get(); if (cachedList?.length > 0) { this.modal.updateData(cachedList); // 自动开始获取粉丝数 this.modal.fetchBatchUsersFansRealtime(cachedList); return; } this.modal.showLoading(MESSAGES.LOADING); const list = await this.fetchFollowList(); if (list.length > 0) { cache.set(list); this.modal.updateData(list); } else { this.modal.showError(MESSAGES.ERROR_NO_DATA); } } catch (error) { console.error('加载失败:', error); if (this.modal) { this.modal.showError('加载失败: ' + error.message); } else { alert('加载失败: ' + error.message); } } finally { this.setLoading(false); } } closeModal() { // 取消所有进行中的操作 this.isOperationCancelled = true; if (this.modal) { this.modal.cancelOperations(); this.modal.remove(); this.modal = null; } // 重置取消状态 this.isOperationCancelled = false; } setLoading(loading) { this.isLoading = loading; this.btn.disabled = loading; }async fetchFollowList() { const uid = getUserId(); if (!uid) throw new Error(MESSAGES.ERROR_NO_UID); let page = 1, result = []; const maxRetries = CONFIG.MAX_RETRIES; while (true) { let success = false; for (let retry = 0; retry < maxRetries && !success; retry++) { try { if (this.modal) { this.modal.showLoading(formatMessage(MESSAGES.LOADING_PAGE, page)); } const res = await fetch(`https://api.bilibili.com/x/relation/followings?vmid=${uid}&pn=${page}&ps=${CONFIG.PAGE_SIZE}&order=desc&order_type=attention`, { credentials: 'include', headers: { 'Referer': 'https://space.bilibili.com/' } }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); if (data.code === 0 && data.data?.list) { // 为新用户初始化粉丝数显示 data.data.list.forEach(user => { if (!user.follower && user.follower !== 0) { user.follower = null; // 标记为未获取 } }); result = result.concat(data.data.list); if (data.data.list.length < CONFIG.PAGE_SIZE) break; page++; success = true; await new Promise(resolve => setTimeout(resolve, CONFIG.API_DELAY)); } else if ([22007, 22115].includes(data.code)) { throw new Error(MESSAGES.ERROR_PERMISSION); } else { throw new Error(formatMessage(MESSAGES.ERROR_API, data.message)); } } catch (error) { console.warn(`获取第${page}页失败 (重试${retry + 1}/${maxRetries}):`, error.message); if (retry < maxRetries - 1) { if (this.modal) { this.modal.showLoading(MESSAGES.RETRY); } await new Promise(resolve => setTimeout(resolve, CONFIG.RETRY_DELAY * Math.pow(2, retry))); } } } if (!success) break; } // 获取完所有数据后一次性更新界面 if (this.modal && result.length > 0) { this.modal.updateData(result); // 开始获取所有用户的粉丝数,并实时更新 this.modal.fetchBatchUsersFansRealtime(result); } return result; } } // 统一的模态窗口类 class FollowModal { constructor(list, onClose) { this.list = list; this.filteredList = list; this.onClose = onClose; this.sortField = 'mtime'; this.sortOrder = 'desc'; this.scrollTimeout = null; this.overlay = null; this.isCancelled = false; this.currentOperations = new Set(); // 跟踪当前进行的操作 this.createModal(); }// 显示状态消息 showMessage(message, isError = false) { const tbody = this.overlay?.querySelector('#table-tbody'); if (tbody) { const icon = isError ? '⚠️' : '
'; const color = isError ? '#dc3545' : '#007bff'; tbody.innerHTML = `
${icon}${message}
`; } } showLoading(message) { this.showMessage(message); } showError(message) { this.showMessage(message, true); } // 更新数据 updateData(newList) { this.list = newList; this.filteredList = newList; const totalCountSpan = this.overlay?.querySelector('#modal-total-count'); if (totalCountSpan) totalCountSpan.textContent = newList.length; this.renderTable(this.filteredList); } // 移除模态窗口 remove() { if (this.overlay) { this.overlay.remove(); this.overlay = null; } } // 取消所有操作 cancelOperations() { this.isCancelled = true; // 统计已获取的数据 const processedUsers = this.list.filter(user => user.follower !== null && user.follower !== undefined); const totalUsers = this.list.length; // 保存已获取的数据到缓存 cache.set(this.list); this.currentOperations.clear(); // 检查overlay是否存在再操作 if (this.overlay) { // 恢复按钮状态 this.setButtonsState(false); this.showCancelButton(false); // 恢复按钮文本 const unfollowBtn = this.overlay.querySelector('#modal-unfollow-selected'); const fetchBtn = this.overlay.querySelector('#modal-fetch-fans'); if (unfollowBtn) unfollowBtn.textContent = '取关选中用户'; if (fetchBtn) fetchBtn.textContent = '获取全部粉丝数'; } // 如果有已获取的数据,显示统计信息 if (processedUsers.length > 0) { setTimeout(() => { alert(`操作已终止\n\n已成功获取 ${processedUsers.length}/${totalUsers} 个用户的粉丝数据\n数据已保存到缓存中`); }, 100); } }createModal() { // 移除已存在的模态窗口 const existingModal = document.querySelector('.modal-overlay'); if (existingModal) { existingModal.remove(); } this.overlay = document.createElement('div'); this.overlay.className = 'modal-overlay'; this.overlay.innerHTML = ` `; document.body.appendChild(this.overlay); this.bindEvents(); } bindEvents() { // 关闭按钮事件 this.overlay.querySelector('.close-btn').onclick = () => { this.cancelOperations(); this.remove(); if (this.onClose) this.onClose(); }; // 点击遮罩层关闭 this.overlay.onclick = (e) => { if (e.target === this.overlay) { this.cancelOperations(); this.remove(); if (this.onClose) this.onClose(); } };// 筛选事件 - 使用防抖优化 ['#modal-filter', '#modal-filter-vip', '#modal-filter-official', '#modal-filter-invalid', '#modal-filter-mutual', '#modal-filter-date-start', '#modal-filter-date-end', '#modal-filter-fans'].forEach(selector => { const el = this.overlay.querySelector(selector); if (el) { const eventType = el.type === 'text' ? 'input' : 'change'; el.addEventListener(eventType, () => { cache.debounce('filter', () => this.filterGrid(), CONFIG.FILTER_DEBOUNCE); }); } }); // 排序事件 this.overlay.querySelectorAll('th.sortable').forEach(th => { th.addEventListener('click', () => this.sortList(th.getAttribute('data-sort'))); }); // 全选事件 ['#modal-select-all', '#table-select-all'].forEach(selector => { const checkbox = this.overlay.querySelector(selector); if (checkbox) checkbox.onchange = () => this.toggleSelectAll(checkbox.checked); }); // 滚动事件 - 自动获取可见用户粉丝数 this.overlay.querySelector('.modal-list').addEventListener('scroll', () => { if (this.scrollTimeout) clearTimeout(this.scrollTimeout); this.scrollTimeout = setTimeout(() => this.fetchVisibleUsersFans(), CONFIG.SCROLL_DEBOUNCE); }); // 批量操作事件 this.overlay.querySelector('#modal-unfollow-selected').onclick = () => this.unfollowSelected(); this.overlay.querySelector('#modal-fetch-fans').onclick = () => this.fetchAllFans(); this.overlay.querySelector('#modal-cancel-operation').onclick = () => this.cancelCurrentOperation(); } // 批量获取用户粉丝数 - 优化版本 async fetchBatchUsersFans(users) { const needFetchUsers = users.filter(user => user.follower === null || user.follower === undefined); if (needFetchUsers.length === 0) return; this.showCancelButton(true); const operationId = 'fetchBatchFans'; this.currentOperations.add(operationId); this.isCancelled = false; const fetchBtn = this.overlay?.querySelector('#modal-fetch-fans'); let processedCount = 0; try { // 分批处理,避免一次性处理太多数据 const batchSize = 10; for (let batchIndex = 0; batchIndex < needFetchUsers.length; batchIndex += batchSize) { if (this.isCancelled) break; const batch = needFetchUsers.slice(batchIndex, batchIndex + batchSize); // 逐个获取粉丝数并实时更新 for (const user of batch) { if (this.isCancelled) break; try { const info = await fetchUserInfo(user.mid); user.follower = info.follower >= 0 ? info.follower : 0; user.following = info.following || 0; // 立即显示粉丝数 this.updateUserRow(user, true); } catch (error) { user.follower = 0; // 立即显示失败状态 this.updateUserRow(user, true); } processedCount++; if (fetchBtn && !this.isCancelled) { const progress = Math.round(processedCount / needFetchUsers.length * 100); fetchBtn.textContent = `获取粉丝数中... ${processedCount}/${needFetchUsers.length} (${progress}%)`; } // 延迟避免频率限制 await new Promise(resolve => setTimeout(resolve, CONFIG.FANS_API_DELAY)); } // 每批次后保存缓存,防止数据丢失 cache.set(this.list); // 批次间额外延迟 if (batchIndex + batchSize < needFetchUsers.length && !this.isCancelled) { await new Promise(resolve => setTimeout(resolve, 500)); } } } finally { this.currentOperations.delete(operationId); // 保存已获取的数据 cache.set(this.list); if (!this.isCancelled) { this.showCancelButton(false); if (fetchBtn) fetchBtn.textContent = '获取全部粉丝数'; } // 如果操作被取消,显示已获取的结果统计 if (this.isCancelled && processedCount > 0) { setTimeout(() => { alert(`批量获取粉丝数已取消\n\n已成功获取 ${processedCount}/${needFetchUsers.length} 个用户的粉丝数据\n数据已保存到缓存中`); }, 100); } } } async fetchVisibleUsersFans() { const fetchBtn = this.overlay.querySelector('#modal-fetch-fans'); if (fetchBtn && fetchBtn.disabled) return; const modalList = this.overlay.querySelector('.modal-list'); const tbody = this.overlay.querySelector('#table-tbody'); const visibleRows = Array.from(tbody.querySelectorAll('tr')).filter(row => { const rect = row.getBoundingClientRect(); const listRect = modalList.getBoundingClientRect(); return rect.top < listRect.bottom && rect.bottom > listRect.top; }); // 限制每次处理的数量,避免一次性请求太多 const maxProcessCount = 5; let processedCount = 0; for (const row of visibleRows) { if (processedCount >= maxProcessCount) break; const checkbox = row.querySelector('.row-checkbox'); if (!checkbox) continue; const mid = checkbox.value; const userInList = this.list.find(u => u.mid === mid); if (userInList && (userInList.follower === null || userInList.follower === undefined)) { try { const info = await fetchUserInfo(mid); if (info.follower >= 0) { userInList.follower = info.follower; userInList.following = info.following; // 立即显示粉丝数 this.updateUserRow(userInList, true); cache.set(this.list); } } catch (error) { userInList.follower = 0; // 立即显示粉丝数0 this.updateUserRow(userInList, true); } processedCount++; await new Promise(resolve => setTimeout(resolve, 300)); } } } // 工具函数 isInvalidUser(user) { return user.face.includes("noface.jpg") && user.uname === "账号已注销"; } formatFollowerCount(count) { return !count ? '0' : count.toLocaleString(); }getOfficialTitle(officialInfo) { if (!officialInfo || officialInfo.type < 0) return ''; const titles = { 0: '个人', 1: '机构' }; const typeTitle = titles[officialInfo.type] || '认证'; return officialInfo.desc ? `${typeTitle}: ${officialInfo.desc}` : typeTitle; } getVipType(vipInfo) { if (!vipInfo || vipInfo.vipType === 0) return ''; return vipInfo.label?.text || '大会员'; } // 筛选功能 - 优化版本 filterGrid() { const filters = this.getFilterState(); const filterKey = JSON.stringify(filters); // 检查缓存 const cached = cache.getFilterResult(filterKey); if (cached) { this.filteredList = cached; this.renderTable(this.filteredList); return; } // 执行筛选 this.filteredList = this.list.filter(user => this.applyFilters(user, filters)); // 缓存结果 cache.setFilterResult(filterKey, this.filteredList); this.renderTable(this.filteredList); } getFilterState() { return { keyword: this.overlay.querySelector('#modal-filter').value.trim().toLowerCase(), vip: this.overlay.querySelector('#modal-filter-vip').value, official: this.overlay.querySelector('#modal-filter-official').value, invalid: this.overlay.querySelector('#modal-filter-invalid').value, mutual: this.overlay.querySelector('#modal-filter-mutual').value, dateStart: this.overlay.querySelector('#modal-filter-date-start').value, dateEnd: this.overlay.querySelector('#modal-filter-date-end').value, fans: this.overlay.querySelector('#modal-filter-fans').value }; } applyFilters(user, filters) { // 快速退出优化 if (filters.keyword) { const nameMatch = user.uname.toLowerCase().includes(filters.keyword); if (!nameMatch) return false; } // 大会员筛选 - 修复并区分年度和月度 if (filters.vip) { const hasVip = user.vip && user.vip.vipType > 0; if (filters.vip === 'false' && hasVip) return false; if (filters.vip === 'annual' && (!hasVip || user.vip.vipType !== 2)) return false; // 年度大会员 if (filters.vip === 'monthly' && (!hasVip || user.vip.vipType !== 1)) return false; // 月度大会员 } // 认证状态筛选 if (filters.official) { const hasOfficial = user.official_verify && user.official_verify.type >= 0; if (filters.official === 'false' && hasOfficial) return false; if (filters.official === '0' && (!hasOfficial || user.official_verify.type !== 0)) return false; if (filters.official === '1' && (!hasOfficial || user.official_verify.type !== 1)) return false; } // 失效账号筛选 if (filters.invalid) { const isInvalid = this.isInvalidUser(user); if (filters.invalid === 'true' && !isInvalid) return false; if (filters.invalid === 'false' && isInvalid) return false; } // 互粉关系筛选 - 修复逻辑错误 if (filters.mutual) { const isMutual = user.attribute === 2; // 2表示互相关注 if (filters.mutual === 'true' && isMutual) return false; // 选择单向关注时排除互粉 if (filters.mutual === 'false' && !isMutual) return false; // 选择互粉时排除单向关注 } // 日期筛选 if ((filters.dateStart || filters.dateEnd) && user.mtime) { const userDate = new Date(user.mtime * 1000).toISOString().split('T')[0]; if (filters.dateStart && userDate < filters.dateStart) return false; if (filters.dateEnd && userDate > filters.dateEnd) return false; } // 粉丝数筛选 if (filters.fans) { const followerCount = user.follower || 0; const ranges = { '1w-': [0, 10000], '1w-10w': [10000, 100000], '10w-100w': [100000, 1000000], '100w+': [1000000, Infinity] }; const range = ranges[filters.fans]; if (range && (followerCount < range[0] || followerCount >= range[1])) return false; } return true; } // 排序功能 sortList(field) { if (this.sortField === field) { this.sortOrder = this.sortOrder === 'desc' ? 'asc' : 'desc'; } else { this.sortField = field; this.sortOrder = 'desc'; } this.filteredList.sort((a, b) => { const valueA = field === 'uname' ? a.uname.toLowerCase() : (a[field] || 0); const valueB = field === 'uname' ? b.uname.toLowerCase() : (b[field] || 0); if (this.sortOrder === 'asc') { return valueA > valueB ? 1 : valueA < valueB ? -1 : 0; } else { return valueA < valueB ? 1 : valueA > valueB ? -1 : 0; } }); // 更新排序标记 this.overlay.querySelectorAll('th').forEach(header => header.classList.remove('sort-asc', 'sort-desc')); const sortHeader = this.overlay.querySelector(`th[data-sort="${field}"]`); if (sortHeader) sortHeader.classList.add(`sort-${this.sortOrder}`); this.renderTable(this.filteredList); } // 表格渲染 renderTable(showList) { const tbody = this.overlay.querySelector('#table-tbody'); const showCountSpan = this.overlay.querySelector('#modal-show-count'); // 限制渲染数量,防止页面卡死 const maxRenderItems = Math.min(showList.length, CONFIG.MAX_RENDER_ITEMS); const displayList = showList.slice(0, maxRenderItems); // 清空表格并使用文档片段优化DOM操作 tbody.innerHTML = ''; const fragment = document.createDocumentFragment(); // 分批渲染,防止长时间阻塞 const renderBatch = (startIndex) => { const batchSize = 50; const endIndex = Math.min(startIndex + batchSize, displayList.length); const batchUsers = displayList.slice(startIndex, endIndex); const batchFragment = this.createTableRows(batchUsers); fragment.appendChild(batchFragment); if (endIndex < displayList.length) { // 使用 requestAnimationFrame 分批渲染 requestAnimationFrame(() => renderBatch(endIndex)); } else { tbody.appendChild(fragment); showCountSpan.textContent = showList.length; if (maxRenderItems < showList.length) { const notice = document.createElement('tr'); notice.innerHTML = ` 仅显示前 ${maxRenderItems} 个结果,请使用筛选功能查看更多内容 `; tbody.appendChild(notice); } this.updateSelectedCount(); } }; if (displayList.length > 0) { renderBatch(0); } else { showCountSpan.textContent = showList.length; this.updateSelectedCount(); } }// 创建表格行的辅助方法 createTableRows(users) { const fragment = document.createDocumentFragment(); users.forEach(user => { const followTime = user.mtime ? new Date(user.mtime * 1000).toLocaleDateString() : '未知'; const isInvalid = this.isInvalidUser(user); // 如果没有粉丝数信息,显示"获取中..." const followerCount = user.follower !== null && user.follower !== undefined ? this.formatFollowerCount(user.follower) : '获取中...'; const officialTitle = this.getOfficialTitle(user.official_verify); const vipType = this.getVipType(user.vip); const row = document.createElement('tr'); row.innerHTML = ` ${user.uname} ${followerCount} ${followTime} ${user.sign || '-'} ${officialTitle} ${vipType} `; // 事件委托优化 - 只在父元素上绑定事件 row.addEventListener('click', this.handleRowClick.bind(this), { passive: true }); fragment.appendChild(row); }); return fragment; } // 行点击事件处理(事件委托) handleRowClick(e) { const row = e.currentTarget; const checkbox = row.querySelector('.row-checkbox'); if (e.target.type !== 'checkbox') { checkbox.checked = !checkbox.checked; } row.classList.toggle('selected', checkbox.checked); this.updateSelectedCount(); } // 全选功能 toggleSelectAll(checked) { const tbody = this.overlay.querySelector('#table-tbody'); const checkboxes = tbody.querySelectorAll('.row-checkbox'); const rows = tbody.querySelectorAll('tr'); checkboxes.forEach((cb, index) => { cb.checked = checked; rows[index].classList.toggle('selected', checked); }); this.updateSelectedCount(); } updateSelectedCount() { const tbody = this.overlay.querySelector('#table-tbody'); const checked = tbody.querySelectorAll('.row-checkbox:checked'); const total = tbody.querySelectorAll('.row-checkbox'); const selectedCountSpan = this.overlay.querySelector('#modal-selected-count'); const tableSelectAll = this.overlay.querySelector('#table-select-all'); selectedCountSpan.textContent = checked.length; tableSelectAll.checked = checked.length > 0 && checked.length === total.length; } // 批量取关 async unfollowSelected() { const tbody = this.overlay.querySelector('#table-tbody'); const checked = tbody.querySelectorAll('.row-checkbox:checked'); if (!checked.length) { alert(MESSAGES.SELECT_USERS); return; } const selectedNames = Array.from(checked).map(cb => cb.getAttribute('data-name')); const confirmText = formatMessage(MESSAGES.CONFIRM_UNFOLLOW, checked.length, selectedNames.slice(0, 10).join('\n') + (selectedNames.length > 10 ? '\n...' : '')); if (!confirm(confirmText)) return; this.setButtonsState(true); this.showCancelButton(true); const unfollowBtn = this.overlay.querySelector('#modal-unfollow-selected'); const operationId = 'unfollowUsers'; this.currentOperations.add(operationId); this.isCancelled = false; unfollowBtn.textContent = '取关中...'; let successCount = 0; const failedUsers = []; try { for (let i = 0; i < checked.length; i++) { if (this.isCancelled) { unfollowBtn.textContent = '操作已取消'; break; } const input = checked[i]; const userName = input.getAttribute('data-name'); try { await unfollowUser(input.value); this.removeUserFromList(input.value); input.closest('tr').remove(); successCount++; if (!this.isCancelled) { unfollowBtn.textContent = formatMessage(MESSAGES.PROGRESS_UNFOLLOW, i + 1, checked.length, successCount); } } catch(e) { console.error(`取关用户 ${userName} 失败:`, e); failedUsers.push(userName); } if (i < checked.length - 1) { await new Promise(resolve => setTimeout(resolve, CONFIG.BATCH_DELAY)); } } this.updateCacheAndCounts(); if (!this.isCancelled) { this.showResult(successCount, failedUsers.length, failedUsers); } else { // 操作被取消时显示已取关的结果 setTimeout(() => { alert(`批量取关已取消\n\n已成功取关 ${successCount} 个用户\n失败 ${failedUsers.length} 个用户`); }, 100); } } catch (error) { console.error('批量取关过程中出现错误:', error); if (!this.isCancelled) { alert('批量取关过程中出现错误,请查看控制台'); } } finally { this.currentOperations.delete(operationId); this.setButtonsState(false); this.showCancelButton(false); if (!this.isCancelled) { unfollowBtn.textContent = '取关选中用户'; } } }// 获取全部粉丝数 async fetchAllFans() { if (!confirm(MESSAGES.CONFIRM_FETCH_ALL)) return; this.setButtonsState(true); this.showCancelButton(true); const btn = this.overlay.querySelector('#modal-fetch-fans'); const operationId = 'fetchAllFans'; this.currentOperations.add(operationId); this.isCancelled = false; btn.textContent = '获取中...'; try { let processedCount = 0; const totalCount = this.list.length; for (let i = 0; i < this.list.length; i++) { if (this.isCancelled) { btn.textContent = '操作已取消'; break; } const user = this.list[i]; try { const info = await fetchUserInfo(user.mid); if (info.follower >= 0) { user.follower = info.follower; user.following = info.following; this.updateUserRow(user); } processedCount++; btn.textContent = formatMessage(MESSAGES.PROGRESS_FETCH, processedCount, totalCount, Math.round(processedCount/totalCount*100)); if (processedCount % 10 === 0) { cache.set(this.list); } } catch (error) { processedCount++; } if (i < this.list.length - 1) { await new Promise(resolve => setTimeout(resolve, CONFIG.API_DELAY)); } } cache.set(this.list); if (!this.isCancelled) { alert(formatMessage(MESSAGES.SUCCESS_FETCH_FANS, processedCount, totalCount)); } else { // 操作被取消时显示已获取的结果 setTimeout(() => { alert(`获取全部粉丝数已取消\n\n已成功获取 ${processedCount}/${totalCount} 个用户的粉丝数据\n数据已保存到缓存中`); }, 100); } } catch (error) { console.error('批量获取粉丝数失败:', error); alert('获取粉丝数时出现错误,请查看控制台'); } finally { this.currentOperations.delete(operationId); this.setButtonsState(false); this.showCancelButton(false); if (!this.isCancelled) { btn.textContent = '获取全部粉丝数'; } } } // 取消当前操作 cancelCurrentOperation() { this.isCancelled = true; cache.set(this.list); this.currentOperations.clear(); this.showCancelButton(false); this.setButtonsState(false); // 恢复按钮文本 const buttons = this.overlay.querySelectorAll('#modal-unfollow-selected, #modal-fetch-fans'); buttons[0] && (buttons[0].textContent = '取关选中用户'); buttons[1] && (buttons[1].textContent = '获取全部粉丝数'); // 显示统计 const processed = this.list.filter(u => u.follower != null).length; if (processed > 0) { alert(`操作已取消\n\n已成功获取 ${processed}/${this.list.length} 个用户的粉丝数据\n数据已保存到缓存中`); } } // 显示/隐藏取消按钮 showCancelButton(show) { const btn = this.overlay?.querySelector('#modal-cancel-operation'); if (btn) btn.style.display = show ? 'inline-block' : 'none'; } // 设置按钮状态 setButtonsState(disabled) { this.overlay?.querySelectorAll('button:not(#modal-cancel-operation)') .forEach(btn => btn.disabled = disabled); } removeUserFromList(mid) { const index = this.list.findIndex(u => u.mid === mid); if (index !== -1) this.list.splice(index, 1); } updateCacheAndCounts() { cache.set(this.list); const rows = this.overlay.querySelector('#table-tbody').children.length; this.overlay.querySelector('#modal-show-count').textContent = rows; this.overlay.querySelector('#modal-total-count').textContent = this.list.length; this.updateSelectedCount(); } showResult(successCount, failCount, failedUsers) { let resultMsg = `批量取关完成!\n成功: ${successCount}个,失败: ${failCount}个`; if (failedUsers.length > 0) { resultMsg += `\n\n失败的用户:\n${failedUsers.slice(0, 5).join('\n')}${failedUsers.length > 5 ? '\n...' : ''}`; } alert(resultMsg); } // 统一的用户行更新方法 updateUserRow(user, isInstant = false) { const tbody = this.overlay?.querySelector('#table-tbody'); if (!tbody) return; const targetRow = Array.from(tbody.querySelectorAll('tr')).find(row => { const checkbox = row.querySelector('.row-checkbox'); return checkbox && checkbox.value === user.mid; }); if (targetRow) { const followerCell = targetRow.children[3]; if (followerCell) { followerCell.innerHTML = this.formatFollowerCount(user.follower); if (isInstant) { // 添加视觉反馈 followerCell.style.background = '#d4edda'; followerCell.style.color = '#155724'; followerCell.style.transition = 'all 0.3s ease'; setTimeout(() => { followerCell.style.background = ''; followerCell.style.color = ''; }, 1500); } } } } // 批量获取用户粉丝数 - 实时更新版本 async fetchBatchUsersFansRealtime(users) { const needFetchUsers = users.filter(user => user.follower === null || user.follower === undefined); if (needFetchUsers.length === 0) return; this.showCancelButton(true); const operationId = 'fetchRealtime'; this.currentOperations.add(operationId); this.isCancelled = false; try { const fetchBtn = this.overlay.querySelector('#modal-fetch-fans'); let processedCount = 0; // 分批处理,避免一次性处理太多数据 const batchSize = 5; for (let batchIndex = 0; batchIndex < needFetchUsers.length; batchIndex += batchSize) { if (this.isCancelled) break; const batch = needFetchUsers.slice(batchIndex, batchIndex + batchSize); for (const user of batch) { if (this.isCancelled) break; try { const info = await fetchUserInfo(user.mid); user.follower = info.follower >= 0 ? info.follower : 0; user.following = info.following || 0; this.updateUserRow(user, true); } catch (error) { user.follower = 0; this.updateUserRow(user, true); } processedCount++; if (fetchBtn && !this.isCancelled) { const progress = Math.round(processedCount / needFetchUsers.length * 100); fetchBtn.textContent = `获取粉丝数中... ${processedCount}/${needFetchUsers.length} (${progress}%)`; } await new Promise(resolve => setTimeout(resolve, CONFIG.FANS_API_DELAY)); } // 每批次后保存缓存,防止数据丢失 if (processedCount % 5 === 0) cache.set(this.list); // 批次间额外延迟 if (batchIndex + batchSize < needFetchUsers.length && !this.isCancelled) { await new Promise(resolve => setTimeout(resolve, 800)); } } if (!this.isCancelled) cache.set(this.list); } finally { this.currentOperations.delete(operationId); cache.set(this.list); if (!this.isCancelled || this.currentOperations.size === 0) { this.showCancelButton(false); const fetchBtn = this.overlay.querySelector('#modal-fetch-fans'); if (fetchBtn) fetchBtn.textContent = '获取全部粉丝数'; } if (this.isCancelled && processedCount > 0) { setTimeout(() => { alert(`粉丝数获取已取消\n\n已成功获取 ${processedCount}/${needFetchUsers.length} 个用户的粉丝数据\n数据已保存到缓存中`); }, 100); } } } } // 初始化应用 new FollowManager(); })();