// ==UserScript== // @name B站关注管理 // @namespace http://tampermonkey.net/ // @version 2.0 // @description 高效管理B站关注列表,支持批量取关、智能筛选等功能。支持智能筛选、实时粉丝数获取、批量操作等功能 // @author 苡淞(Yis_Rime) // @homepage https://github.com/YisRime/BilibiliFollowManage // @match https://space.bilibili.com/*/relation/follow* // @match https://space.bilibili.com/*/fans/follow* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @connect api.bilibili.com // @license AGPLv3 // @downloadURL https://update.greasyfork.icu/scripts/537856/B%E7%AB%99%E5%85%B3%E6%B3%A8%E7%AE%A1%E7%90%86.user.js // @updateURL https://update.greasyfork.icu/scripts/537856/B%E7%AB%99%E5%85%B3%E6%B3%A8%E7%AE%A1%E7%90%86.meta.js // ==/UserScript== (function() { 'use strict'; // 基础配置 const CONFIG = { API_DELAY: 500, PAGE_SIZE: 50, BATCH_DELAY: 300, CACHE_DURATION: 30 * 24 * 60 * 60 * 1000, FANS_API_DELAY: 500 }; // 提示消息 const MESSAGES = { LOADING: '正在加载数据...', ERROR_NO_DATA: '未能获取关注列表,请检查是否已登录', ERROR_NO_UID: '无法获取用户UID,请确保已登录', CONFIRM_UNFOLLOW: '确定要取关选中的用户吗?此操作不可撤销!', 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; } .follow-manager-btn:hover { background: #0088cc; transform: translateY(-2px); } .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: 90vw; max-width: 1200px; height: 80vh; max-height: 800px; overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 20px 40px rgba(0,0,0,0.15); } .modal-header { padding: 20px; 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: 18px; font-weight: 600; } .close-btn { background: none; border: none; font-size: 24px; cursor: pointer; color: #fff; 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: nowrap; } .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: 140px; } .filter-row input[type="date"] { min-width: 80px; } .filter-row input[type="number"] { min-width: 60px; width: 60px; } .filter-row select { min-width: 80px; } .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; } .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; } .follow-table th:nth-child(4) { min-width: 100px; } .follow-table th:nth-child(8) { min-width: 120px; } .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; } .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; } `); // 工具函数 const getUserId = () => { const uidMatch = location.pathname.match(/space\/(\d+)/); return uidMatch?.[1] || document.cookie.match(/DedeUserID=(\d+)/)?.[1] || null; }; const getCsrfToken = () => document.cookie.match(/bili_jct=([^;]+)/)?.[1] || ''; // 缓存管理 const cache = { get() { const cached = localStorage.getItem('bilibili_follow_cache'); if (!cached) return null; const data = JSON.parse(cached); return data.expiry > Date.now() ? data.list : null; }, set(list) { localStorage.setItem('bilibili_follow_cache', JSON.stringify({ list, expiry: Date.now() + CONFIG.CACHE_DURATION })); }, getUserInfo(mid) { const cached = localStorage.getItem(`bilibili_user_${mid}`); if (!cached) return null; const data = JSON.parse(cached); return (data.timestamp && Date.now() - data.timestamp < CONFIG.CACHE_DURATION) ? data : null; }, setUserInfo(mid, info) { localStorage.setItem(`bilibili_user_${mid}`, JSON.stringify({...info, timestamp: Date.now()})); }, clear() { localStorage.removeItem('bilibili_follow_cache'); }, clearUserInfo() { let count = 0; for (let i = localStorage.length - 1; i >= 0; i--) { const key = localStorage.key(i); if (key && key.startsWith('bilibili_user_')) { localStorage.removeItem(key); count++; } } return count; }, removeUserInfo(mid) { localStorage.removeItem(`bilibili_user_${mid}`); } }; // 获取用户粉丝数信息 const fetchUserInfo = async (mid) => { const cached = cache.getUserInfo(mid); if (cached) return cached; try { const res = await fetch(`https://api.bilibili.com/x/relation/stat?vmid=${mid}`, { credentials: 'include', headers: { 'Referer': 'https://space.bilibili.com/' } }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); let info; if (data.code === 0 && data.data) { info = { follower: data.data.follower || 0, following: data.data.following || 0, timestamp: Date.now() }; } else if (data.code === -404 || data.code === -400) { info = { follower: -1, following: -1, timestamp: Date.now() }; } else { info = { follower: 0, following: 0, timestamp: Date.now() }; } cache.setUserInfo(mid, info); return info; } catch (error) { const defaultInfo = { follower: 0, following: 0, timestamp: Date.now() }; cache.setUserInfo(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) { const data = JSON.parse(response.responseText); data.code === 0 ? resolve(data) : reject(new Error(data.message || '取关失败')); }, onerror: () => reject(new Error('网络请求失败')) }); }); }; // 主管理类 class SimpleFollowManager { constructor() { this.modal = null; this.isLoading = false; this.followList = []; this.filteredList = []; this.sortField = 'mtime'; this.sortOrder = 'desc'; 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.createModal(); // 首先尝试从缓存获取数据 const cachedList = cache.get(); if (cachedList?.length > 0) { this.followList = cachedList; this.filteredList = cachedList; this.renderTable(); // 在后台获取粉丝数信息 this.fetchBatchUsersFansRealtime(cachedList); } else { this.showLoading(MESSAGES.LOADING); const list = await this.fetchFollowList(); if (list.length > 0) { cache.set(list); this.followList = list; this.filteredList = list; this.renderTable(); } else { this.showError(MESSAGES.ERROR_NO_DATA); } } } catch (error) { console.error('加载失败:', error); this.showError('加载失败: ' + error.message); } finally { this.setLoading(false); } } closeModal() { if (this.modal) { this.modal.remove(); this.modal = null; } } 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; let result = []; while (true) { 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) { result = result.concat(data.data.list); if (data.data.list.length < CONFIG.PAGE_SIZE) break; page++; await new Promise(resolve => setTimeout(resolve, CONFIG.API_DELAY)); } else { throw new Error(data.message || 'API错误'); } } return result; } createModal() { this.modal = document.createElement('div'); this.modal.className = 'modal-overlay'; this.modal.innerHTML = `
`; document.body.appendChild(this.modal); this.bindEvents(); } bindEvents() { // 关闭按钮 this.modal.querySelector('.close-btn').onclick = () => this.closeModal(); this.modal.onclick = (e) => {if (e.target === this.modal) this.closeModal()}; // 筛选 this.modal.querySelector('#filter-input').oninput = () => this.filterList(); this.modal.querySelector('#filter-status').onchange = () => this.filterList(); this.modal.querySelector('#filter-vip').onchange = () => this.filterList(); this.modal.querySelector('#filter-official').onchange = () => this.filterList(); this.modal.querySelector('#filter-date-start').onchange = () => this.filterList(); this.modal.querySelector('#filter-date-end').onchange = () => this.filterList(); this.modal.querySelector('#filter-fans-min').oninput = () => this.filterList(); this.modal.querySelector('#filter-fans-max').oninput = () => this.filterList(); // 排序 this.modal.querySelectorAll('th.sortable').forEach(th => { th.addEventListener('click', () => this.sortList(th.getAttribute('data-sort'))); }); // 全选 this.modal.querySelector('#table-select-all').onchange = (e) => this.toggleSelectAll(e.target.checked); // 取关 this.modal.querySelector('#unfollow-btn').onclick = () => this.unfollowSelected(); // 获取粉丝数和清除缓存 this.modal.querySelector('#fetch-fans-btn').onclick = () => this.fetchAllFans(); this.modal.querySelector('#clear-cache-btn').onclick = () => this.clearCache(); } showLoading(message) { const tbody = this.modal?.querySelector('#table-tbody'); if (tbody) tbody.innerHTML = `