TOOLS为非官方社区,欢迎一起建设,答疑群:717421103

版本260427.1

全站通知:

油猴工具/批量回退及预览差异

阅读

    

2026-04-27更新

    

最新编辑:WIKI不易猫猫叹气

阅读:

  

更新日期:2026-04-27

  

最新编辑:WIKI不易猫猫叹气

来自ToolsWIKI_BWIKI_哔哩哔哩
跳到导航 跳到搜索
页面贡献者 :
WIKI不易猫猫叹气
油猴工具/批量回退及预览差异
小工具稳定
用于bwiki的特殊:最近更改页面,支持多选记录进行批量回退和悬浮预览差异,按需魔改
作者
WIKI不易猫猫叹气
系列油猴工具
版本1.0.0

// ==UserScript==
// @name         批量回退及预览差异
// @namespace    http://tampermonkey.net/
// @version      2025-08-26
// @description  用于bwiki的[[特殊:最近更改]]页面,支持多选记录进行批量回退和悬浮预览差异,按需魔改
// @author       AI、长亭 
// @match        *://wiki.biligame.com/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    console.log('===[批量回退工具] 脚本===');
    // 等待页面加载完成
    if (document.readyState === 'loading') {
        console.log('[批量回退工具] 页面加载中,等待DOM加载完成');
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

    /**
     * 初始化函数,设置所有功能
     */
    function init() {
        // 尝试获取目标列表(支持两种结构)
        let targetElement = document.querySelector('ul.mw-contributions-list');
        let isTableStructure = false;

        // 如果没找到ul列表,尝试查找表格结构
        if (!targetElement) {
            targetElement = document.querySelector('.mw-changeslist');
            if (targetElement) {
                isTableStructure = true;
                console.log('[批量回退工具] 找到表格结构的编辑历史');
            } else {
                console.log('[批量回退工具] 未找到编辑历史元素');
                return;
            }
        }

        // 为符合条件的元素添加勾选框
        addCheckboxes(targetElement, isTableStructure);

        // 添加批量操作按钮
        addBatchButtons();

        // 添加差异悬停预览功能
        addDiffHoverEffect();
    }

    /**
     * 为符合条件的元素添加勾选框
     * @param {HTMLElement} container - 目标容器元素
     * @param {boolean} isTableStructure - 是否为表格结构
     */
    function addCheckboxes(container, isTableStructure) {
        if (isTableStructure) {
            // 处理表格结构
            const tableRows = container.querySelectorAll('table.mw-changeslist-line tr');
            tableRows.forEach(tr => {
                // 只处理包含mw-rollback-link的行
                if (tr.querySelector('.mw-rollback-link')) {
                    // 找到第一个单元格
                    const firstCell = tr.querySelector('td');
                    if (firstCell) {
                        // 创建勾选框
                        const checkbox = document.createElement('input');
                        checkbox.type = 'checkbox';
                        checkbox.className = 'batch-rollback-checkbox';
                        checkbox.style.marginRight = '10px';

                        // 将勾选框添加到单元格的开头
                        firstCell.insertBefore(checkbox, firstCell.firstChild);
                    }
                }
            });
        } else {
            // 处理列表结构
            const listItems = container.querySelectorAll('li');
            listItems.forEach(li => {
                // 只处理包含mw-rollback-link的li
                if (li.querySelector('.mw-rollback-link')) {
                    // 创建勾选框
                    const checkbox = document.createElement('input');
                    checkbox.type = 'checkbox';
                    checkbox.className = 'batch-rollback-checkbox';
                    checkbox.style.marginRight = '10px';

                    // 将勾选框添加到li的开头
                    li.insertBefore(checkbox, li.firstChild);
                }
            });
        }
    }

    /**
     * 添加批量操作按钮(批量勾选和批量回退)
     */
    function addBatchButtons() {
        // 创建按钮容器
        const buttonContainer = document.createElement('div');
        buttonContainer.className = 'batch-rollback-container';
        buttonContainer.style.position = 'fixed';
        buttonContainer.style.bottom = '20px';
        buttonContainer.style.right = '20px';
        buttonContainer.style.zIndex = '9999';
        buttonContainer.style.display = 'flex';
        buttonContainer.style.flexDirection = 'column';
        buttonContainer.style.gap = '10px';

        // 创建批量勾选按钮
        const selectAllButton = document.createElement('button');
        selectAllButton.className = 'batch-select-button';
        selectAllButton.textContent = '批量勾选';
        selectAllButton.style.padding = '8px 15px';
        selectAllButton.style.backgroundColor = '#4CAF50';
        selectAllButton.style.color = 'white';
        selectAllButton.style.border = 'none';
        selectAllButton.style.borderRadius = '4px';
        selectAllButton.style.cursor = 'pointer';
        selectAllButton.style.fontWeight = 'bold';

        // 批量勾选按钮状态(0: 默认, 1: 全选)
        let selectState = 0;

        // 批量勾选按钮点击事件
        selectAllButton.addEventListener('click', () => {
            const checkboxes = document.querySelectorAll('.batch-rollback-checkbox');
            if (selectState === 0) {
                // 全选
                checkboxes.forEach(checkbox => {
                    checkbox.checked = true;
                });
                selectAllButton.textContent = '取消全选';
                selectState = 1;
            } else {
                // 取消全选
                checkboxes.forEach(checkbox => {
                    checkbox.checked = false;
                });
                selectAllButton.textContent = '批量勾选';
                selectState = 0;
            }
        });

        // 创建批量回退按钮
        const batchRollbackButton = document.createElement('button');
        batchRollbackButton.className = 'batch-rollback-button';
        batchRollbackButton.textContent = '批量回退';
        batchRollbackButton.style.padding = '8px 15px';
        batchRollbackButton.style.backgroundColor = '#f44336';
        batchRollbackButton.style.color = 'white';
        batchRollbackButton.style.border = 'none';
        batchRollbackButton.style.borderRadius = '4px';
        batchRollbackButton.style.cursor = 'pointer';
        batchRollbackButton.style.fontWeight = 'bold';

        // 批量回退按钮点击事件
        batchRollbackButton.addEventListener('click', () => {
            performBatchRollback();
        });

        // 添加进度条容器
        const progressContainer = document.createElement('div');
        progressContainer.className = 'batch-rollback-progress-container';
        progressContainer.style.width = '200px';
        progressContainer.style.height = '20px';
        progressContainer.style.backgroundColor = '#f1f1f1';
        progressContainer.style.borderRadius = '10px';
        progressContainer.style.overflow = 'hidden';
        progressContainer.style.display = 'none';

        // 添加进度条
        const progressBar = document.createElement('div');
        progressBar.className = 'batch-rollback-progress-bar';
        progressBar.style.height = '100%';
        progressBar.style.backgroundColor = '#4CAF50';
        progressBar.style.width = '0%';
        progressBar.style.transition = 'width 0.3s ease';

        // 添加进度文本
        const progressText = document.createElement('div');
        progressText.className = 'batch-rollback-progress-text';
        progressText.style.position = 'absolute';
        progressText.style.width = '200px';
        progressText.style.textAlign = 'center';
        progressText.style.lineHeight = '20px';
        progressText.style.color = '#000';
        progressText.style.fontSize = '12px';
        progressText.textContent = '0%';

        // 组装进度条
        progressContainer.appendChild(progressBar);
        progressContainer.appendChild(progressText);

        // 创建预加载进度显示文本
        const preloadProgressText = document.createElement('div');
        preloadProgressText.className = 'preload-progress-text';
        preloadProgressText.style.textAlign = 'center';
        preloadProgressText.style.color = '#333';
        preloadProgressText.style.fontSize = '14px';
        preloadProgressText.textContent = '差异预加载 0/0';

        // 组装按钮容器
        buttonContainer.appendChild(progressContainer);
        buttonContainer.appendChild(selectAllButton);
        buttonContainer.appendChild(batchRollbackButton);
        buttonContainer.appendChild(preloadProgressText);

        // 添加到页面
        document.body.appendChild(buttonContainer);
    }

    /**
     * 执行批量回退操作
     */
    function performBatchRollback() {
        const checkboxes = document.querySelectorAll('.batch-rollback-checkbox:checked');
        if (checkboxes.length === 0) {
            alert('请先选择要回退的编辑');
            return;
        }

        // 获取进度条元素
        const progressContainer = document.querySelector('.batch-rollback-progress-container');
        const progressBar = document.querySelector('.batch-rollback-progress-bar');
        const progressText = document.querySelector('.batch-rollback-progress-text');

        // 显示进度条
        progressContainer.style.display = 'block';
        progressBar.style.width = '0%';
        progressText.textContent = '0%';

        // 计算每次回退后进度条增加的百分比
        const progressIncrement = 100 / checkboxes.length;
        let currentProgress = 0;

        // 递归函数执行回退操作
        function processNext(index) {
            if (index >= checkboxes.length) {
                // 所有回退完成
                progressBar.style.width = '100%';
                progressText.textContent = '100%';
                setTimeout(() => {
                    progressContainer.style.display = 'none';
                    alert('批量回退操作已完成');
                }, 500);
                return;
            }

            // 获取当前勾选框所在的行(支持li或tr)
            const parentElement = checkboxes[index].closest('li, tr');
            if (parentElement) {
                // 找到回退按钮
                const rollbackLink = parentElement.querySelector('.mw-rollback-link a');
                if (rollbackLink) {
                    // 在新标签页打开回退链接并监控状态
                    // 创建隐藏iframe执行回退操作
                    const iframe = document.createElement('iframe');
                    iframe.style.display = 'none';
                    document.body.appendChild(iframe);

                    // iframe加载完成后处理
                    iframe.onload = function() {
                        // 删除iframe元素
                        document.body.removeChild(iframe);

                        // 更新进度条
                        currentProgress += progressIncrement;
                        progressBar.style.width = `${currentProgress}%`;
                        progressText.textContent = `${Math.round(currentProgress)}%`;

                        // 设置延迟以确保操作完成
                        setTimeout(() => {
                            processNext(index + 1);
                        }, 1000);
                    };

                    // 设置iframe源为回退链接
                    iframe.src = rollbackLink.href;
                    return;
                }
            }

            // 等待1秒后处理下一个
            setTimeout(() => {
                processNext(index + 1);
            }, 1000);
        }

        // 开始处理第一个
        processNext(0);
    }
    /**
     * 为差异按钮添加鼠标悬停预览效果
     */
    function addDiffHoverEffect() {


        /**
         * 预加载差异内容
         * @param {HTMLAnchorElement} diffLink - 差异链接元素
         */
        function preloadDiffContent(diffLink) {
            const url = diffLink.href;
            if (diffCache.has(url) || loadingUrls.has(url)) return;

            loadingUrls.add(url);

            // 创建隐藏iframe加载差异内容
            const iframe = document.createElement('iframe');
            iframe.style.display = 'none';
            iframe.src = url;

            iframe.onload = () => {
                try {
                    const diffTable = iframe.contentDocument.querySelector('table.diff');
                    if (diffTable) {
                        // 存储完整的表格HTML
                        diffCache.set(url, diffTable.outerHTML);
                    }
                } catch (e) {
                    console.error('[批量回退工具] 预加载差异内容失败:', e);
                }
                loadingUrls.delete(url);
                iframe.remove();
            };

            // 错误处理
            iframe.onerror = () => {
                loadingUrls.delete(url);
                iframe.remove();
            };

            document.body.appendChild(iframe);
        }

        /**
         * 初始化差异内容预加载机制
         */
        function initDiffPreloading() {
            // 计算总链接数
            const allDiffLinks = document.querySelectorAll('a.mw-changeslist-diff');
            totalLinks = allDiffLinks.length;
            updatePreloadProgress();

            // 使用Intersection Observer监测元素进入视口
            const observer = new IntersectionObserver((entries) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                const diffLink = entry.target.querySelector('a.mw-changeslist-diff');
                if (diffLink && !diffCache.has(diffLink.href) && !loadingUrls.has(diffLink.href) && !preloadQueue.includes(diffLink)) {
                    preloadQueue.push(diffLink);
                    processPreloadQueue(); // 触发队列处理
                }
                // 添加到队列后取消观察,避免重复添加
                observer.unobserve(entry.target);
            }
                });
            }, {
                rootMargin: '2000px 0px', // 提前2000px开始预加载(扩大预加载范围)
                threshold: 0
            });

            // 将所有差异按钮容器添加到观察列表
            document.querySelectorAll('.mw-changeslist-links').forEach(container => {
                observer.observe(container);
            });
        }

        // 缓存差异内容的对象,键为URL,值为差异表格HTML
        const diffCache = new Map();
        // 存储正在加载的URL,避免重复请求
        const loadingUrls = new Set();
        // 预加载队列
        const preloadQueue = [];
        // 获取预加载进度显示元素
        const preloadProgressText = document.querySelector('.preload-progress-text');
        // 总链接数
        let totalLinks = 0;
        // 已加载链接数
        let loadedLinks = 0;

        // 初始化预加载机制
        initDiffPreloading();
        // 是否正在加载中
        let isLoading = false;

        /**
         * 处理预加载队列,串行加载差异内容
         */
        function processPreloadQueue() {
            if (isLoading || preloadQueue.length === 0) return;

            isLoading = true;
            const diffLink = preloadQueue.shift();
            if (!diffLink) {
                isLoading = false;
                return;
            }

            preloadDiffContent(diffLink).then(() => {
                loadedLinks++;
                updatePreloadProgress();
                isLoading = false;
                processPreloadQueue(); // 加载完成后处理下一个
            }).catch(() => {
                isLoading = false;
                processPreloadQueue(); // 出错也继续处理下一个
            });
        }

        /**
         * 更新预加载进度显示
         */
        function updatePreloadProgress() {
            if (preloadProgressText) {
                preloadProgressText.textContent = `差异预加载 ${loadedLinks}/${totalLinks}`;
            }
        }

        /**
         * 预加载差异内容(返回Promise)
         * @param {HTMLAnchorElement} diffLink - 差异链接元素
         */
        function preloadDiffContent(diffLink) {
            return new Promise((resolve, reject) => {
                const url = diffLink.href;
                if (diffCache.has(url) || loadingUrls.has(url)) {
                    resolve();
                    return;
                }

                loadingUrls.add(url);

                // 创建隐藏iframe加载差异内容
                const iframe = document.createElement('iframe');
                iframe.style.display = 'none';
                iframe.src = url;

                iframe.onload = () => {
                    try {
                        const diffTable = iframe.contentDocument.querySelector('table.diff');
                        if (diffTable) {
                            // 存储完整的表格HTML
                            diffCache.set(url, diffTable.outerHTML);
                        }
                        resolve();
                    } catch (e) {
                        console.error('[批量回退工具] 预加载差异内容失败:', e);
                        reject(e);
                    } finally {
                        loadingUrls.delete(url);
                        iframe.remove();
                    }
                };

                // 错误处理
                iframe.onerror = () => {
                    loadingUrls.delete(url);
                    iframe.remove();
                    reject(new Error('Failed to load iframe'));
                };

                document.body.appendChild(iframe);
            });
        }
        // 为所有差异按钮容器添加事件监听
        document.querySelectorAll('.mw-changeslist-links').forEach(linkContainer => {
            const diffLink = linkContainer.querySelector('a.mw-changeslist-diff');
            if (!diffLink) return;

            let infobox = null;
            let iframe = null;

            // 鼠标悬停时创建预览框
            linkContainer.addEventListener('mouseenter', (e) => {
                // 创建信息框元素
                infobox = document.createElement('div');
                infobox.className = 'diff-preview-infobox';
                infobox.style.cssText = `
                    position: fixed;
                    top: ${0}px;
                    right: ${0}px;
                    z-index: 9998;
                    width: calc(50% - 50px);
                    height: 100vh;
                    background: white;
                    border: 1px solid #ccc;
                    border-radius: ${0}px;
                    box-shadow: -2px 2px 10px rgba(0,0,0,0.2);
                    overflow-y: auto;
                    padding: 10px;
                `;
                infobox.textContent = '加载差异内容中...';
                document.body.appendChild(infobox);

                // 检查缓存
                    const cachedDiff = diffCache.get(diffLink.href);
                    if (cachedDiff) {
                        // 使用缓存 content
                        infobox.innerHTML = cachedDiff;
                        const diffTable = infobox.querySelector('table.diff');
                        if (diffTable) {
                            diffTable.style.width = '100%';
                        }
                    } else {
                        // 缓存未命中时加载
                        infobox.textContent = '加载差异中...';
                        iframe = document.createElement('iframe');
                        iframe.style.display = 'none';
                        iframe.src = diffLink.href;
                        iframe.onload = () => {
                            try {
                                const diffTable = iframe.contentDocument.querySelector('table.diff');
                                if (diffTable) {
                                    diffTable.style.width = '100%';
                                    infobox.innerHTML = '';
                                    infobox.appendChild(diffTable);
                                    // 更新缓存
                                    diffCache.set(diffLink.href, diffTable.outerHTML);
                                } else {
                                    infobox.textContent = '未找到差异表格';
                                }
                            } catch (e) {
                                infobox.textContent = '无法加载差异内容: ' + e.message;
                            }
                            iframe.remove();
                        };
                        document.body.appendChild(iframe);
                    }
            });

            // 鼠标离开时移除预览框
            linkContainer.addEventListener('mouseleave', () => {
                if (infobox) infobox.remove();
                if (iframe) iframe.remove();
            });
        });
    }

})();