B站黑名单便捷管理

在B站空间页面便捷地管理您的黑名单,支持分页浏览、一键移除,扫描不活跃用户(3个月未更新、已封禁、已注销)。

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         B站黑名单便捷管理
// @namespace    http://tampermonkey.net/
// @version      0.0.1
// @description  在B站空间页面便捷地管理您的黑名单,支持分页浏览、一键移除,扫描不活跃用户(3个月未更新、已封禁、已注销)。
// @author       yingming006
// @match        https://space.bilibili.com/*
// @icon         https://www.bilibili.com/favicon.ico
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @connect      api.bilibili.com
// @connect      app.biliapi.com
// @license      GPL-3.0 License
// ==/UserScript==

(function() {
    'use strict';

    // --- 全局变量 ---
    let currentPage = 1, pageSize = 10, totalUsers = 0, totalPages = 1, csrfToken = '', isFetchingVideo = {}, intersectionObserver = null;
    let isScanning = false;

    // --- CSS样式 ---
    GM_addStyle(`
        /* Bilibili Style Color Palette & Variables */
        :root {
            --bili-blue: #00aeec;
            --bili-blue-hover: #00b5e5;
            --bili-red: #f44336;
            --bili-red-hover: #e53935;
            --bili-orange: #ff9800;
            --bili-orange-hover: #fb8c00;
            --bili-text-main: #18191c;
            --bili-text-light: #61666d;
            --bili-text-lighter: #9499a0;
            --bili-bg-light: #fff;
            --bili-border-color: #e3e5e7;
            --bili-bg-hover: #f1f2f3;
            --bili-bg-disabled: #e3e5e7;
            --bili-text-disabled: #b8c0cc;
        }

        /* Modal Base */
        #blacklist-modal, #inactive-scan-modal {
            position: fixed; z-index: 9999; left: 0; top: 0; width: 100%; height: 100%;
            background-color: rgba(0, 0, 0, 0.4);
            display: none; justify-content: center; align-items: center;
        }
        #blacklist-modal-content, #inactive-scan-modal-content {
            background-color: var(--bili-bg-light);
            padding: 24px;
            width: clamp(500px, 60vw, 800px);
            max-height: 85vh;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
            display: flex; flex-direction: column;
        }

        /* Modal Header */
        #blacklist-modal-header, #inactive-scan-modal-header {
            display: flex; justify-content: space-between; align-items: center;
            border-bottom: 1px solid var(--bili-border-color);
            padding-bottom: 16px; margin-bottom: 16px;
        }
        #blacklist-modal-header h2, #inactive-scan-modal-header h2 {
            font-size: 18px; color: var(--bili-text-main); font-weight: 500;
        }
        #blacklist-modal-close, #inactive-scan-modal-close {
            color: var(--bili-text-lighter); font-size: 24px; font-weight: bold;
            cursor: pointer; transition: color 0.2s;
        }
        #blacklist-modal-close:hover, #inactive-scan-modal-close:hover {
            color: var(--bili-text-light);
        }

        /* List Container & Items */
        #blacklist-list-container, #inactive-list-container {
            flex-grow: 1; overflow-y: auto; margin-right: -8px; padding-right: 8px;
        }
        .blacklist-item {
            display: flex; align-items: center; gap: 15px;
            padding: 12px 8px;
            border-bottom: 1px solid var(--bili-border-color);
            transition: background-color 0.2s;
        }
        .blacklist-item:last-child { border-bottom: none; }
        .blacklist-item:hover { background-color: var(--bili-bg-hover); }
        .blacklist-item img { width: 48px; height: 48px; border-radius: 50%; flex-shrink: 0; }

        /* User Info */
        .info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
        .info a { text-decoration: none; color: var(--bili-blue); font-weight: 500; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 14px; }
        .info a:hover { color: var(--bili-blue-hover); }
        .info span { color: var(--bili-text-lighter); font-size: 12px; display: block; }

        /* Video/Status Info */
        .latest-video-info { display: flex; flex-direction: column; justify-content: center; flex: 2; min-width: 0; font-size: 13px; color: var(--bili-text-light); line-height: 1.5; }
        .latest-video-info a { color: var(--bili-text-light); text-decoration: none; }
        .latest-video-info a:hover { color: var(--bili-blue); }
        .latest-video-info .video-title { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%; }
        .user-status { font-weight: bold; text-align: left; font-size: 13px; }
        .status-banned { color: var(--bili-orange); }
        .status-cancelled { color: var(--bili-text-lighter); }

        /* General Button Style */
        .btn-remove, #blacklist-pagination button, #stopScanBtn {
            padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer;
            font-size: 14px; color: white; transition: background-color 0.2s;
            flex-shrink: 0;
        }
        .btn-remove:disabled, #blacklist-pagination button:disabled, #stopScanBtn:disabled {
            background-color: var(--bili-bg-disabled) !important;
            color: var(--bili-text-disabled); cursor: not-allowed;
        }
        .btn-remove { background-color: var(--bili-red); }
        .btn-remove:hover:not(:disabled) { background-color: var(--bili-red-hover); }

        /* Pagination & Scan Button */
        #blacklist-pagination {
            margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--bili-border-color);
            text-align: center; display: flex; justify-content: center; align-items: center; gap: 10px; flex-wrap: wrap;
            color: var(--bili-text-light);
        }
        #blacklist-pagination button { background-color: var(--bili-blue); }
        #blacklist-pagination button:hover:not(:disabled) { background-color: var(--bili-blue-hover); }
        #scanInactiveBtn { background-color: var(--bili-orange); margin-left: auto; }
        #scanInactiveBtn:hover:not(:disabled) { background-color: var(--bili-orange-hover); }
        #stopScanBtn { background-color: var(--bili-red); }
        #stopScanBtn:hover:not(:disabled) { background-color: var(--bili-red-hover); }
        #pageJumpInput {
            width: 50px; text-align: center; border: 1px solid var(--bili-border-color);
            border-radius: 4px; padding: 5px; font-size: 14px;
            background-color: var(--bili-bg-light);
            transition: border-color 0.2s, box-shadow 0.2s;
        }
        #pageJumpInput:focus { border-color: var(--bili-blue); outline: none; box-shadow: 0 0 0 2px rgba(0, 174, 236, 0.2); }

        /* Utility */
        .spinner { width: 18px; height: 18px; border: 2px solid var(--bili-bg-hover); border-top: 2px solid var(--bili-blue); border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto; }
        @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
        .toast-notification { position: fixed; top: 20px; right: 20px; padding: 12px 20px; border-radius: 6px; color: white; font-size: 14px; z-index: 10000; opacity: 0; transition: opacity 0.3s, top 0.3s; box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
        .toast-notification.show { top: 40px; opacity: 1; }
        .toast-notification.success { background-color: #4CAF50; }
        .toast-notification.error { background-color: #f44336; }
        #scan-status { padding: 10px; text-align: center; font-style: italic; color: var(--bili-text-light); }
        #blacklist-message { padding: 40px; text-align: center; color: var(--bili-text-light); }
    `);

    // --- 辅助函数 ---
    function getCsrfToken() { const cookies = document.cookie.split(';'); for (let c of cookies) { c = c.trim(); if (c.startsWith('bili_jct=')) return c.substring('bili_jct='.length); } return ''; }
    function showToast(message, type = 'success') { const t = document.createElement('div'); t.className = `toast-notification ${type}`; t.textContent = message; document.body.appendChild(t); setTimeout(() => t.classList.add('show'), 10); setTimeout(() => { t.classList.remove('show'); setTimeout(() => document.body.removeChild(t), 300); }, 3000); }
    function formatTimestamp(unixTimestamp) { if (!unixTimestamp) return '未知时间'; return new Date(unixTimestamp * 1000).toLocaleDateString(); }

    // --- API请求 ---
    async function fetchBlacklist(page = 1) {
        showLoadingMessage('正在加载黑名单...');
        const apiUrl = `https://api.bilibili.com/x/relation/blacks?pn=${page}&ps=${pageSize}`;
        try {
            const response = await fetch(apiUrl, { credentials: 'include' });
            const data = await response.json();
            if (data.code === 0) {
                totalUsers = data.data.total;
                totalPages = Math.ceil(totalUsers / pageSize) || 1;
                currentPage = page;
                renderBlacklist(data.data.list);
                renderPagination();
            } else { throw new Error(`API错误: ${data.message}`); }
        } catch (error) { console.error('查询黑名单失败:', error); showLoadingMessage(`查询失败: ${error.message}`); }
    }

    async function removeFromBlacklist(mid, uname, onSuccess) {
        showToast(`正在移除【${uname}】...`, 'success');
        const apiUrl = 'https://api.bilibili.com/x/relation/modify';
        const formData = new URLSearchParams({ fid: mid, act: 6, re_src: 11, csrf: csrfToken });
        try {
            const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formData, credentials: 'include' });
            const data = await response.json();
            if (data.code === 0) {
                showToast(`用户【${uname}】已成功移出黑名单!`, 'success');
                if (onSuccess) onSuccess();
            } else { throw new Error(`API错误: ${data.message}`); }
        } catch (error) { console.error('移除失败:', error); showToast(`移除失败: ${error.message}`, 'error'); }
    }

    function fetchLatestVideo(mid, container) {
        if (isFetchingVideo[mid]) return;
        isFetchingVideo[mid] = true;
        fetchLatestVideoPromise(mid).then(result => {
            if (result && result.video) {
                const video = result.video;
                const pubDate = new Date(video.ctime * 1000).toLocaleDateString();
                container.innerHTML = `<div><a class="video-title" href="//www.bilibili.com/video/${video.bvid}" target="_blank" title="${video.title}">${video.title}</a><span style="color: var(--bili-text-lighter);">发布于: ${pubDate}</span></div>`;
            } else {
                container.textContent = result.message;
            }
        }).finally(() => { delete isFetchingVideo[mid]; });
    }

    function fetchLatestVideoPromise(mid) {
        return new Promise((resolve) => {
            const params = new URLSearchParams({ vmid: mid, ps: 1, order: 'pubdate', build: '8430300', mobi_app: 'android', platform: 'android', qn: 80 });
            GM_xmlhttpRequest({
                method: "GET", url: `https://app.biliapi.com/x/v2/space/archive/cursor?${params.toString()}`,
                headers: { 'User-Agent': 'Mozilla/5.0' }, anonymous: true,
                onload: function(response) {
                    try {
                        const data = JSON.parse(response.responseText);
                        if (data.code === 0 && data.data.item && data.data.item.length > 0) resolve({ video: data.data.item[0], message: '成功' });
                        else resolve({ video: null, message: '未找到或隐藏了动态' });
                    } catch (e) { resolve({ video: null, message: '数据解析失败' }); }
                },
                onerror: function() { resolve({ video: null, message: '获取失败' }); }
            });
        });
    }

    // --- 渲染函数 ---
    function renderBlacklist(users) {
        const container = document.getElementById('blacklist-list-container');
        container.innerHTML = '';
        if (intersectionObserver) intersectionObserver.disconnect();
        if (!users || users.length === 0) { container.innerHTML = '<div id="blacklist-message">黑名单中没有用户。</div>'; return; }

        intersectionObserver = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    const target = entry.target;
                    const mid = target.dataset.mid;
                    fetchLatestVideo(mid, target);
                    intersectionObserver.unobserve(target);
                }
            });
        }, { root: container });

        users.forEach(user => {
            const faceUrl = user.face.startsWith('//') ? 'https:' + user.face : user.face;
            const userSpaceUrl = `https://space.bilibili.com/${user.mid}`;
            const item = document.createElement('div');
            item.className = 'blacklist-item';

            const isBanned = user.uname.startsWith('bili_') && user.uname === `bili_${user.mid}`;
            const isCancelled = user.uname === '账号已注销';
            let statusOrVideoHTML = isBanned ? `<div class="latest-video-info"><span class="user-status status-banned">状态: 已封禁</span></div>`
                : isCancelled ? `<div class="latest-video-info"><span class="user-status status-cancelled">状态: 已注销</span></div>`
                : `<div class="latest-video-info" data-mid="${user.mid}"><div class="spinner"></div></div>`;

            const addTime = formatTimestamp(user.mtime);
            const userInfoHTML = `
                <div class="info">
                    <a href="${userSpaceUrl}" target="_blank" title="${user.uname}">${user.uname}</a>
                    <span>MID: ${user.mid}</span>
                    <span>拉黑于: ${addTime}</span>
                </div>`;

            item.innerHTML = `<a href="${userSpaceUrl}" target="_blank"><img src="${faceUrl.replace('http:', 'https:')}" alt="${user.uname}的头像"></a>${userInfoHTML}${statusOrVideoHTML}<button class="btn-remove">移除</button>`;
            container.appendChild(item);
            const removeBtn = item.querySelector('.btn-remove');
            removeBtn.onclick = () => { removeFromBlacklist(user.mid, user.uname, () => { removeBtn.textContent = '已移除'; removeBtn.disabled = true; }); };
            if (!isBanned && !isCancelled) intersectionObserver.observe(item.querySelector('.latest-video-info'));
        });
    }

    function renderPagination() {
        const container = document.getElementById('blacklist-pagination');
        if (totalUsers === 0) { container.innerHTML = ''; return; }
        container.innerHTML = `<button id="scanInactiveBtn">扫描不活跃用户</button><div><button id="prevPageBtn">上一页</button><span style="margin: 0 8px;">第</span><input type="number" id="pageJumpInput" value="${currentPage}" min="1" max="${totalPages}"><span> / ${totalPages} 页 (共 ${totalUsers} 人)</span><button id="jumpPageBtn" style="margin-left:8px;">跳转</button><button id="nextPageBtn" style="margin-left:8px;">下一页</button></div>`;
        document.getElementById('prevPageBtn').disabled = currentPage <= 1;
        document.getElementById('nextPageBtn').disabled = currentPage >= totalPages;
        document.getElementById('prevPageBtn').onclick = () => fetchBlacklist(currentPage - 1);
        document.getElementById('nextPageBtn').onclick = () => fetchBlacklist(currentPage + 1);
        document.getElementById('scanInactiveBtn').onclick = scanForInactiveUsers;
        const jumpInput = document.getElementById('pageJumpInput');
        const jumpBtn = document.getElementById('jumpPageBtn');
        const jumpToPage = () => {
            let pageNum = parseInt(jumpInput.value, 10);
            if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= totalPages) { fetchBlacklist(pageNum); }
            else { showToast(`请输入 1 到 ${totalPages} 之间的有效页码!`, 'error'); jumpInput.value = currentPage; }
        };
        jumpBtn.onclick = jumpToPage;
        jumpInput.onkeydown = (event) => { if (event.key === 'Enter') { event.preventDefault(); jumpToPage(); } };
    }

    function showLoadingMessage(msg) {
        document.getElementById('blacklist-list-container').innerHTML = `<div id="blacklist-message">${msg}</div>`;
        document.getElementById('blacklist-pagination').innerHTML = '';
    }

    // --- 不活跃用户扫描功能 ---
    async function scanForInactiveUsers() {
        if (isScanning) { showToast('扫描已在进行中...', 'error'); return; }
        isScanning = true;

        const scanModal = document.getElementById('inactive-scan-modal');
        const scanContainer = document.getElementById('inactive-list-container');
        const scanStatus = document.getElementById('scan-status');
        const stopScanBtn = document.getElementById('stopScanBtn');

        scanModal.style.display = 'flex';
        scanContainer.innerHTML = '';
        stopScanBtn.style.display = 'inline-block';
        stopScanBtn.disabled = false;
        stopScanBtn.textContent = '中止扫描';

        let inactiveUsersFoundCount = 0;
        let scanPage = 1;
        const threeMonthsAgo = new Date();
        threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);

        while (scanPage <= totalPages && isScanning) {
            scanStatus.textContent = `正在扫描第 ${scanPage}/${totalPages} 页... 已找到 ${inactiveUsersFoundCount} 位不活跃用户...`;
            try {
                const response = await fetch(`https://api.bilibili.com/x/relation/blacks?pn=${scanPage}&ps=${pageSize}`, { credentials: 'include' });
                const data = await response.json();
                if (data.code !== 0) throw new Error(data.message);

                const usersToCheckVideo = [];
                for (const user of data.data.list) {
                    if (!isScanning) break;
                    const isBanned = user.uname.startsWith('bili_') && user.uname === `bili_${user.mid}`;
                    const isCancelled = user.uname === '账号已注销';
                    if (isBanned || isCancelled) {
                        inactiveUsersFoundCount++;
                        renderInactiveUser(user, null);
                    } else {
                        usersToCheckVideo.push(user);
                    }
                }

                if (isScanning && usersToCheckVideo.length > 0) {
                    const videoPromises = usersToCheckVideo.map(user => fetchLatestVideoPromise(user.mid).then(result => ({ user, result })));
                    const results = await Promise.all(videoPromises);
                    for (const { user, result } of results) {
                        if (!isScanning) break;
                        if (result.video) {
                            const videoDate = new Date(result.video.ctime * 1000);
                            if (videoDate < threeMonthsAgo) {
                                inactiveUsersFoundCount++;
                                renderInactiveUser(user, result.video);
                            }
                        } else {
                            // 如果用户没有视频动态,也视为不活跃
                            inactiveUsersFoundCount++;
                            renderInactiveUser(user, null, '未找到动态');
                        }
                    }
                }
                scanPage++;
            } catch (error) {
                scanStatus.textContent = `扫描出错: ${error.message}。已停止。`;
                isScanning = false;
            }
        }

        if (!isScanning && scanPage <= totalPages) {
            scanStatus.textContent = `扫描已中止。共扫描 ${scanPage - 1} 页,找到 ${inactiveUsersFoundCount} 位不活跃用户。`;
        } else if (inactiveUsersFoundCount === 0) {
            scanStatus.textContent = `扫描完成。在 ${totalPages} 页中未找到不活跃用户。`;
        } else {
            scanStatus.textContent = `扫描完成!共找到 ${inactiveUsersFoundCount} 位不活跃用户。`;
        }
        isScanning = false;
        stopScanBtn.style.display = 'none';
    }

    function renderInactiveUser(user, video, message = '') {
        const container = document.getElementById('inactive-list-container');
        const item = document.createElement('div');
        item.className = 'blacklist-item';
        item.id = `inactive-user-${user.mid}`;
        const faceUrl = user.face.startsWith('//') ? 'https:' + user.face : user.face;
        const userSpaceUrl = `https://space.bilibili.com/${user.mid}`;

        let statusOrVideoHTML = '';
        if (video) {
            const pubDate = new Date(video.ctime * 1000).toLocaleDateString();
            statusOrVideoHTML = `<div class="latest-video-info"><div><a class="video-title" href="//www.bilibili.com/video/${video.bvid}" target="_blank" title="${video.title}">${video.title}</a><span style="color: var(--bili-text-lighter);">最后更新: ${pubDate} (超过3个月)</span></div></div>`;
        } else {
            const isBanned = user.uname.startsWith('bili_') && user.uname === `bili_${user.mid}`;
            if (isBanned) {
                statusOrVideoHTML = `<div class="latest-video-info"><span class="user-status status-banned">状态: 已封禁</span></div>`;
            } else if(user.uname === '账号已注销') {
                statusOrVideoHTML = `<div class="latest-video-info"><span class="user-status status-cancelled">状态: 已注销</span></div>`;
            } else {
                statusOrVideoHTML = `<div class="latest-video-info"><span class="user-status status-cancelled">状态: ${message || '无动态'}</span></div>`;
            }
        }

        const addTime = formatTimestamp(user.mtime);
        const userInfoHTML = `
            <div class="info">
                <a href="${userSpaceUrl}" target="_blank" title="${user.uname}">${user.uname}</a>
                <span>MID: ${user.mid}</span>
                <span>拉黑于: ${addTime}</span>
            </div>`;

        item.innerHTML = `<a href="${userSpaceUrl}" target="_blank"><img src="${faceUrl.replace('http:', 'https:')}" alt="${user.uname}的头像"></a>${userInfoHTML}${statusOrVideoHTML}<button class="btn-remove">移除</button>`;
        container.appendChild(item);
        const removeBtn = item.querySelector('.btn-remove');
        removeBtn.onclick = () => { removeFromBlacklist(user.mid, user.uname, () => { removeBtn.textContent = '已移除'; removeBtn.disabled = true; }); };
    }


    // --- 初始化 ---
    function init() {
        const modal = document.createElement('div');
        modal.id = 'blacklist-modal';
        modal.innerHTML = `<div id="blacklist-modal-content"><div id="blacklist-modal-header"><h2>黑名单管理</h2><span id="blacklist-modal-close">&times;</span></div><div id="blacklist-list-container"></div><div id="blacklist-pagination"></div></div>`;
        document.body.appendChild(modal);

        const scanModal = document.createElement('div');
        scanModal.id = 'inactive-scan-modal';
        scanModal.innerHTML = `
            <div id="inactive-scan-modal-content">
                <div id="inactive-scan-modal-header">
                    <h2>不活跃用户扫描结果</h2>
                    <div>
                        <button id="stopScanBtn" style="display:none;">中止扫描</button>
                        <span id="inactive-scan-modal-close" style="margin-left: 15px;">&times;</span>
                    </div>
                </div>
                <div id="scan-status">点击主面板的扫描按钮开始...</div>
                <div id="inactive-list-container"></div>
            </div>`;
        document.body.appendChild(scanModal);

        const floatBtn = document.createElement('button');
        floatBtn.textContent = '管理黑名单';
        Object.assign(floatBtn.style, {
            position: 'fixed',
            bottom: '120px',
            left: '30px',
            zIndex: '9998',
            padding: '10px 15px',
            backgroundColor: 'var(--bili-blue)',
            color: 'white',
            border: 'none',
            borderRadius: '8px',
            cursor: 'pointer',
            boxShadow: '0 2px 10px rgba(0,0,0,0.2)',
            fontSize: '14px',
            transition: 'background-color 0.2s'
        });
        floatBtn.onmouseover = () => floatBtn.style.backgroundColor = 'var(--bili-blue-hover)';
        floatBtn.onmouseout = () => floatBtn.style.backgroundColor = 'var(--bili-blue)';
        document.body.appendChild(floatBtn);

        floatBtn.onclick = () => {
            csrfToken = getCsrfToken();
            if (!csrfToken) { showToast('获取登录信息失败,请确保您已登录B站!', 'error'); return; }
            isFetchingVideo = {};
            modal.style.display = 'flex';
            fetchBlacklist(1);
        };

        const closeModal = () => { modal.style.display = 'none'; };
        document.getElementById('blacklist-modal-close').onclick = closeModal;
        modal.onclick = (event) => { if (event.target == modal) closeModal(); };

        const stopScanBtn = document.getElementById('stopScanBtn');
        const closeScanModal = () => {
            scanModal.style.display = 'none';
            if(isScanning) {
                isScanning = false;
                showToast('扫描已中止', 'success');
            }
        };
        stopScanBtn.onclick = () => {
            if(isScanning) {
                isScanning = false;
                stopScanBtn.disabled = true;
                stopScanBtn.textContent = '正在中止...';
                showToast('扫描将在当前页面完成后中止...', 'success');
            }
        };
        document.getElementById('inactive-scan-modal-close').onclick = closeScanModal;
        scanModal.onclick = (event) => { if (event.target == scanModal) closeScanModal(); };
    }

    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        init();
    } else {
        window.addEventListener('DOMContentLoaded', init);
    }
})();