雀魂牌谱屋对局数据采集脚本

保存所有加载的对局数据

// ==UserScript==
// @name         雀魂牌谱屋对局数据采集脚本
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  保存所有加载的对局数据
// @author       藤田
// @license      MIT
// @match        https://amae-koromo.sapk.ch/*
// @match        https://saki.sapk.ch/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // 创建样式
    const style = document.createElement('style');
    style.textContent = `
        .auto-scroll-container {
            position: fixed;
            top: 10px;
            right: 10px;
            z-index: 9999;
            background-color: #f8f9fa;
            border: 1px solid #ddd;
            border-radius: 4px;
            padding: 10px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            max-width: 300px;
            font-size: 14px;
        }
        .auto-scroll-container button {
            margin: 5px;
            padding: 5px 10px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            transition: background-color 0.3s;
        }
        .auto-scroll-container button:hover {
            background-color: #45a049;
        }
        .auto-scroll-container button:disabled {
            background-color: #cccccc;
            cursor: not-allowed;
        }
        .progress-bar-container {
            width: 100%;
            background-color: #e0e0e0;
            border-radius: 4px;
            margin-top: 10px;
            height: 10px;
        }
        .progress-bar {
            height: 100%;
            background-color: #4CAF50;
            border-radius: 4px;
            width: 0%;
            transition: width 0.3s;
        }
        .status-indicator {
            position: fixed;
            bottom: 10px;
            right: 10px;
            background-color: #4CAF50;
            color: white;
            padding: 5px 10px;
            border-radius: 4px;
            z-index: 9999;
            font-size: 12px;
            transition: opacity 0.3s;
        }
        .minimize-button {
            position: absolute;
            top: 5px;
            right: 5px;
            cursor: pointer;
            font-size: 16px;
            color: #666;
        }
        .minimize-button:hover {
            color: #000;
        }
        .auto-scroll-container.minimized {
            width: 30px;
            height: 30px;
            overflow: hidden;
            padding: 0;
        }
        .expand-button {
            width: 100%;
            height: 100%;
            display: flex;
            justify-content: center;
            align-items: center;
            font-size: 16px;
            color: #666;
        }
        .advanced-options {
            margin-top: 10px;
            padding-top: 10px;
            border-top: 1px solid #ddd;
            font-size: 12px;
        }
        .advanced-options-toggle {
            color: #0066cc;
            cursor: pointer;
            text-decoration: underline;
            display: block;
            margin-bottom: 5px;
        }
        .advanced-options-content {
            display: none;
        }
        .advanced-options-content.visible {
            display: block;
        }
        .scroll-stats {
            margin-top: 5px;
            font-size: 11px;
            color: #666;
        }
        .memory-stats {
            margin-top: 5px;
            font-size: 11px;
            color: #666;
        }
        .pause-resume-button {
            background-color: #ff9800 !important;
        }
        .pause-resume-button:hover {
            background-color: #e68a00 !important;
        }
        .export-progress {
            margin-top: 5px;
            font-size: 11px;
            color: #666;
            display: none;
        }
    `;
    document.head.appendChild(style);

    // 存储所有已加载的数据
    let allPreservedData = [];
    let isScrolling = false;
    let isPaused = false;
    let scrollInterval = null;
    let observer = null;
    let lastDataLength = 0;
    let isMinimized = false;
    let scrollSpeed = 1; // 默认滚动速度
    let maxScrollAttempts = 10000; // 最大滚动尝试次数,设置为极高的值
    let scrollAttempts = 0;
    let noNewDataCounter = 0;
    let autoStopThreshold = 20; // 连续多少次没有新数据时自动停止,增加阈值
    let pauseBeforeScroll = 500; // 每次滚动前暂停时间(毫秒)
    let batchSize = 100; // 数据批处理大小
    let memoryOptimization = true; // 是否启用内存优化
    let showAdvancedOptions = false; // 是否显示高级选项
    let lastScrollPosition = 0; // 上次滚动位置
    let samePositionCounter = 0; // 相同位置计数器
    let samePositionThreshold = 5; // 连续多少次相同位置时判断为无法继续滚动
    let totalScrollDistance = 0; // 总滚动距离
    let totalDataCollected = 0; // 总采集数据量
    let scrollStartTime = null; // 滚动开始时间
    let memoryCheckInterval = null; // 内存检查间隔
    let dataChunks = []; // 数据分块存储
    let currentChunkIndex = 0; // 当前数据块索引
    let chunkSize = 500; // 每个数据块的大小
    let autoSaveInterval = null; // 自动保存间隔
    let autoSaveEnabled = true; // 是否启用自动保存
    let autoSaveMinutes = 5; // 自动保存间隔(分钟)
    let lastAutoSaveTime = null; // 上次自动保存时间
    let processedRowIds = new Set(); // 已处理的行ID集合

    // 节流函数,限制函数调用频率
    function throttle(func, limit) {
        let lastFunc;
        let lastRan;
        return function() {
            const context = this;
            const args = arguments;
            if (!lastRan) {
                func.apply(context, args);
                lastRan = Date.now();
            } else {
                clearTimeout(lastFunc);
                lastFunc = setTimeout(function() {
                    if ((Date.now() - lastRan) >= limit) {
                        func.apply(context, args);
                        lastRan = Date.now();
                    }
                }, limit - (Date.now() - lastRan));
            }
        };
    }

    // 防抖函数,延迟函数执行
    function debounce(func, delay) {
        let debounceTimer;
        return function() {
            const context = this;
            const args = arguments;
            clearTimeout(debounceTimer);
            debounceTimer = setTimeout(() => func.apply(context, args), delay);
        };
    }

    // 获取当前内存使用情况
    function getMemoryUsage() {
        if (window.performance && window.performance.memory) {
            return {
                total: Math.round(window.performance.memory.totalJSHeapSize / (1024 * 1024)),
                used: Math.round(window.performance.memory.usedJSHeapSize / (1024 * 1024)),
                limit: Math.round(window.performance.memory.jsHeapSizeLimit / (1024 * 1024))
            };
        }
        return null;
    }

    // 更新内存使用统计
    function updateMemoryStats() {
        const memoryStatsDiv = document.getElementById('memory-stats');
        if (!memoryStatsDiv) return;

        const memoryUsage = getMemoryUsage();
        if (memoryUsage) {
            memoryStatsDiv.textContent = `内存: ${memoryUsage.used}MB / ${memoryUsage.total}MB (${Math.round(memoryUsage.used / memoryUsage.total * 100)}%)`;

            // 如果内存使用率超过80%,触发垃圾回收和数据优化
            if (memoryUsage.used / memoryUsage.total > 0.8) {
                optimizeMemoryUsage();
            }
        } else {
            memoryStatsDiv.textContent = '内存统计不可用';
        }
    }

    // 优化内存使用
    function optimizeMemoryUsage() {
        if (!memoryOptimization) return;

        // 如果当前数据块已满,创建新数据块
        if (allPreservedData.length >= chunkSize) {
            // 将当前数据块添加到数据块数组
            dataChunks.push([...allPreservedData]);
            currentChunkIndex = dataChunks.length;

            // 清空当前数据数组,释放内存
            allPreservedData = [];

            // 强制垃圾回收(尽管JavaScript没有直接的垃圾回收API)
            setTimeout(() => {
                // 这里不做任何事情,只是给垃圾回收一个机会
            }, 0);

            updateStatus(`已创建数据块 #${currentChunkIndex},共 ${getTotalDataCount()} 条数据`);
        }
    }

    // 获取总数据数量
    function getTotalDataCount() {
        let total = allPreservedData.length;
        dataChunks.forEach(chunk => {
            total += chunk.length;
        });
        return total;
    }

    // 获取所有数据(合并所有数据块)
    function getAllData() {
        let allData = [];
        dataChunks.forEach(chunk => {
            allData = allData.concat(chunk);
        });
        allData = allData.concat(allPreservedData);
        return allData;
    }

    // 等待页面加载完成
    window.addEventListener('load', function() {
        // 创建控制面板
        createControlPanel();

        // 初始化数据保存功能
        initDataPreservation();

        // 启动内存监控
        startMemoryMonitoring();

        // 初始化自动保存时间
        lastAutoSaveTime = new Date();
    });

    // 启动内存监控
    function startMemoryMonitoring() {
        if (memoryCheckInterval) {
            clearInterval(memoryCheckInterval);
        }

        // 每10秒检查一次内存使用情况
        memoryCheckInterval = setInterval(() => {
            updateMemoryStats();

            // 检查是否需要自动保存
            if (autoSaveEnabled && isScrolling && !isPaused && lastAutoSaveTime) {
                const now = new Date();
                const minutesSinceLastSave = (now - lastAutoSaveTime) / (1000 * 60);

                if (minutesSinceLastSave >= autoSaveMinutes) {
                    autoSaveData();
                }
            }
        }, 10000);
    }

    // 自动保存数据
    function autoSaveData() {
        if (getTotalDataCount() === 0) return;

        const timestamp = new Date().toISOString().replace(/:/g, '-').slice(0, 19);
        const filename = `雀魂牌谱数据_自动保存_${timestamp}.json`;

        // 创建一个隐藏的下载链接
        const jsonContent = 'data:text/json;charset=utf-8,' +
                           encodeURIComponent(JSON.stringify(getAllData(), null, 2));

        const link = document.createElement('a');
        link.setAttribute('href', jsonContent);
        link.setAttribute('download', filename);
        link.style.display = 'none';
        document.body.appendChild(link);

        // 触发下载
        link.click();
        document.body.removeChild(link);

        lastAutoSaveTime = new Date();
        updateStatus(`已自动保存 ${getTotalDataCount()} 条数据`);
    }

    // 创建控制面板
    function createControlPanel() {
        const container = document.createElement('div');
        container.className = 'auto-scroll-container';

        // 添加最小化按钮
        const minimizeButton = document.createElement('div');
        minimizeButton.className = 'minimize-button';
        minimizeButton.textContent = '−';
        minimizeButton.title = '最小化';
        minimizeButton.onclick = toggleMinimize;
        container.appendChild(minimizeButton);

        // 创建内容容器
        const contentContainer = document.createElement('div');
        contentContainer.className = 'content-container';

        const title = document.createElement('h3');
        title.textContent = '雀魂牌谱屋对局数据采集';
        title.style.margin = '0 0 10px 0';
        title.style.fontSize = '14px';
        contentContainer.appendChild(title);

        // 滚动速度控制
        const speedContainer = document.createElement('div');
        speedContainer.style.marginBottom = '10px';

        const speedLabel = document.createElement('label');
        speedLabel.textContent = '滚动速度: ';
        speedLabel.style.fontSize = '12px';
        speedContainer.appendChild(speedLabel);

        const speedSelect = document.createElement('select');
        speedSelect.style.marginLeft = '5px';
        speedSelect.style.padding = '2px';

        const speeds = [
            { value: 0.2, text: '超慢速' },
            { value: 0.5, text: '慢速' },
            { value: 1, text: '正常' },
            { value: 2, text: '快速' },
            { value: 3, text: '极速' }
        ];

        speeds.forEach(speed => {
            const option = document.createElement('option');
            option.value = speed.value;
            option.textContent = speed.text;
            if (speed.value === 1) option.selected = true;
            speedSelect.appendChild(option);
        });

        speedSelect.onchange = function() {
            scrollSpeed = parseFloat(this.value);

            // 如果正在滚动,更新滚动间隔
            if (isScrolling && !isPaused && scrollInterval) {
                clearInterval(scrollInterval);
                startScrollInterval();
            }
        };

        speedContainer.appendChild(speedSelect);
        contentContainer.appendChild(speedContainer);

        // 开始按钮
        const startButton = document.createElement('button');
        startButton.textContent = '开始完全遍历';
        startButton.onclick = function() {
            startAutoScroll();
            this.disabled = true;
            stopButton.disabled = false;
            pauseResumeButton.disabled = false;
            updateStatus('正在自动滚动并采集数据...');
        };
        contentContainer.appendChild(startButton);

        // 停止按钮
        const stopButton = document.createElement('button');
        stopButton.textContent = '停止滚动';
        stopButton.disabled = true;
        stopButton.onclick = function() {
            stopAutoScroll();
            this.disabled = true;
            startButton.disabled = false;
            pauseResumeButton.disabled = true;
            pauseResumeButton.textContent = '暂停滚动';
            isPaused = false;
            updateStatus(`已停止滚动,共采集 ${getTotalDataCount()} 条数据`);
        };
        contentContainer.appendChild(stopButton);

        // 暂停/恢复按钮
        const pauseResumeButton = document.createElement('button');
        pauseResumeButton.textContent = '暂停滚动';
        pauseResumeButton.className = 'pause-resume-button';
        pauseResumeButton.disabled = true;
        pauseResumeButton.onclick = function() {
            if (isPaused) {
                // 恢复滚动
                isPaused = false;
                this.textContent = '暂停滚动';
                startScrollInterval();
                updateStatus('已恢复滚动');
            } else {
                // 暂停滚动
                isPaused = true;
                this.textContent = '恢复滚动';
                if (scrollInterval) {
                    clearInterval(scrollInterval);
                    scrollInterval = null;
                }
                updateStatus('已暂停滚动');
            }
        };
        contentContainer.appendChild(pauseResumeButton);

        // 复制按钮
        const copyButton = document.createElement('button');
        copyButton.textContent = '复制所有数据';
        copyButton.onclick = copyAllData;
        contentContainer.appendChild(copyButton);

        // 清除按钮
        const clearButton = document.createElement('button');
        clearButton.textContent = '清除数据';
        clearButton.onclick = function() {
            if (confirm('确定要清除所有已采集的数据吗?')) {
                allPreservedData = [];
                dataChunks = [];
                currentChunkIndex = 0;
                lastDataLength = 0;
                totalDataCollected = 0;
                processedRowIds.clear();
                updateStatus('已清除所有数据');
                updateProgressBar(0);
                updateScrollStats();
                updateMemoryStats();
            }
        };
        contentContainer.appendChild(clearButton);

        // 进度条
        const progressContainer = document.createElement('div');
        progressContainer.className = 'progress-bar-container';

        const progressBar = document.createElement('div');
        progressBar.className = 'progress-bar';
        progressBar.id = 'scroll-progress-bar';

        progressContainer.appendChild(progressBar);
        contentContainer.appendChild(progressContainer);

        // 滚动统计信息
        const scrollStatsDiv = document.createElement('div');
        scrollStatsDiv.className = 'scroll-stats';
        scrollStatsDiv.id = 'scroll-stats';
        scrollStatsDiv.innerHTML = '滚动次数: 0 | 滚动距离: 0px | 运行时间: 0:00';
        contentContainer.appendChild(scrollStatsDiv);

        // 内存统计信息
        const memoryStatsDiv = document.createElement('div');
        memoryStatsDiv.className = 'memory-stats';
        memoryStatsDiv.id = 'memory-stats';
        memoryStatsDiv.textContent = '内存统计加载中...';
        contentContainer.appendChild(memoryStatsDiv);

        // 数据统计信息
        const statsDiv = document.createElement('div');
        statsDiv.style.fontSize = '12px';
        statsDiv.style.marginTop = '5px';
        statsDiv.style.color = '#666';
        statsDiv.id = 'data-stats';
        statsDiv.textContent = '已采集 0 条数据';
        contentContainer.appendChild(statsDiv);

        // 导出进度
        const exportProgressDiv = document.createElement('div');
        exportProgressDiv.className = 'export-progress';
        exportProgressDiv.id = 'export-progress';
        exportProgressDiv.textContent = '导出进度: 0%';
        contentContainer.appendChild(exportProgressDiv);

        // 高级选项
        const advancedOptions = document.createElement('div');
        advancedOptions.className = 'advanced-options';

        const advancedToggle = document.createElement('span');
        advancedToggle.className = 'advanced-options-toggle';
        advancedToggle.textContent = '显示高级选项';
        advancedToggle.onclick = function() {
            showAdvancedOptions = !showAdvancedOptions;
            advancedContent.classList.toggle('visible', showAdvancedOptions);
            this.textContent = showAdvancedOptions ? '隐藏高级选项' : '显示高级选项';
        };
        advancedOptions.appendChild(advancedToggle);

        const advancedContent = document.createElement('div');
        advancedContent.className = 'advanced-options-content';

        // 暂停时间设置
        const pauseContainer = document.createElement('div');
        pauseContainer.style.marginBottom = '5px';

        const pauseLabel = document.createElement('label');
        pauseLabel.textContent = '滚动间隔(毫秒): ';
        pauseLabel.style.fontSize = '12px';
        pauseContainer.appendChild(pauseLabel);

        const pauseInput = document.createElement('input');
        pauseInput.type = 'number';
        pauseInput.min = '100';
        pauseInput.max = '2000';
        pauseInput.step = '100';
        pauseInput.value = pauseBeforeScroll;
        pauseInput.style.width = '60px';
        pauseInput.onchange = function() {
            pauseBeforeScroll = parseInt(this.value) || 500;
        };
        pauseContainer.appendChild(pauseInput);
        advancedContent.appendChild(pauseContainer);

        // 自动停止阈值设置
        const thresholdContainer = document.createElement('div');
        thresholdContainer.style.marginBottom = '5px';

        const thresholdLabel = document.createElement('label');
        thresholdLabel.textContent = '自动停止阈值: ';
        thresholdLabel.style.fontSize = '12px';
        thresholdContainer.appendChild(thresholdLabel);

        const thresholdInput = document.createElement('input');
        thresholdInput.type = 'number';
        thresholdInput.min = '5';
        thresholdInput.max = '50';
        thresholdInput.step = '1';
        thresholdInput.value = autoStopThreshold;
        thresholdInput.style.width = '40px';
        thresholdInput.onchange = function() {
            autoStopThreshold = parseInt(this.value) || 20;
        };
        thresholdContainer.appendChild(thresholdInput);
        advancedContent.appendChild(thresholdContainer);

        // 相同位置阈值设置
        const samePositionContainer = document.createElement('div');
        samePositionContainer.style.marginBottom = '5px';

        const samePositionLabel = document.createElement('label');
        samePositionLabel.textContent = '相同位置阈值: ';
        samePositionLabel.style.fontSize = '12px';
        samePositionContainer.appendChild(samePositionLabel);

        const samePositionInput = document.createElement('input');
        samePositionInput.type = 'number';
        samePositionInput.min = '3';
        samePositionInput.max = '20';
        samePositionInput.step = '1';
        samePositionInput.value = samePositionThreshold;
        samePositionInput.style.width = '40px';
        samePositionInput.onchange = function() {
            samePositionThreshold = parseInt(this.value) || 5;
        };
        samePositionContainer.appendChild(samePositionInput);
        advancedContent.appendChild(samePositionContainer);

        // 数据块大小设置
        const chunkSizeContainer = document.createElement('div');
        chunkSizeContainer.style.marginBottom = '5px';

        const chunkSizeLabel = document.createElement('label');
        chunkSizeLabel.textContent = '数据块大小: ';
        chunkSizeLabel.style.fontSize = '12px';
        chunkSizeContainer.appendChild(chunkSizeLabel);

        const chunkSizeInput = document.createElement('input');
        chunkSizeInput.type = 'number';
        chunkSizeInput.min = '100';
        chunkSizeInput.max = '2000';
        chunkSizeInput.step = '100';
        chunkSizeInput.value = chunkSize;
        chunkSizeInput.style.width = '60px';
        chunkSizeInput.onchange = function() {
            chunkSize = parseInt(this.value) || 500;
        };
        chunkSizeContainer.appendChild(chunkSizeInput);
        advancedContent.appendChild(chunkSizeContainer);

        // 自动保存设置
        const autoSaveContainer = document.createElement('div');
        autoSaveContainer.style.marginBottom = '5px';

        const autoSaveCheckbox = document.createElement('input');
        autoSaveCheckbox.type = 'checkbox';
        autoSaveCheckbox.id = 'auto-save';
        autoSaveCheckbox.checked = autoSaveEnabled;
        autoSaveCheckbox.onchange = function() {
            autoSaveEnabled = this.checked;
            autoSaveMinutesInput.disabled = !this.checked;
        };
        autoSaveContainer.appendChild(autoSaveCheckbox);

        const autoSaveLabel = document.createElement('label');
        autoSaveLabel.htmlFor = 'auto-save';
        autoSaveLabel.textContent = ' 启用自动保存,间隔(分钟): ';
        autoSaveLabel.style.fontSize = '12px';
        autoSaveContainer.appendChild(autoSaveLabel);

        const autoSaveMinutesInput = document.createElement('input');
        autoSaveMinutesInput.type = 'number';
        autoSaveMinutesInput.min = '1';
        autoSaveMinutesInput.max = '60';
        autoSaveMinutesInput.step = '1';
        autoSaveMinutesInput.value = autoSaveMinutes;
        autoSaveMinutesInput.style.width = '40px';
        autoSaveMinutesInput.disabled = !autoSaveEnabled;
        autoSaveMinutesInput.onchange = function() {
            autoSaveMinutes = parseInt(this.value) || 5;
        };
        autoSaveContainer.appendChild(autoSaveMinutesInput);
        advancedContent.appendChild(autoSaveContainer);

        // 内存优化选项
        const memoryContainer = document.createElement('div');

        const memoryCheckbox = document.createElement('input');
        memoryCheckbox.type = 'checkbox';
        memoryCheckbox.id = 'memory-optimization';
        memoryCheckbox.checked = memoryOptimization;
        memoryCheckbox.onchange = function() {
            memoryOptimization = this.checked;
        };
        memoryContainer.appendChild(memoryCheckbox);

        const memoryLabel = document.createElement('label');
        memoryLabel.htmlFor = 'memory-optimization';
        memoryLabel.textContent = ' 启用内存优化';
        memoryLabel.style.fontSize = '12px';
        memoryContainer.appendChild(memoryLabel);
        advancedContent.appendChild(memoryContainer);

        // 导出选项
        const exportContainer = document.createElement('div');
        exportContainer.style.marginTop = '5px';

        const exportButton = document.createElement('button');
        exportButton.textContent = '导出为CSV';
        exportButton.style.fontSize = '12px';
        exportButton.style.padding = '3px 6px';
        exportButton.onclick = exportToCsv;
        exportContainer.appendChild(exportButton);

        const jsonButton = document.createElement('button');
        jsonButton.textContent = '导出为JSON';
        jsonButton.style.fontSize = '12px';
        jsonButton.style.padding = '3px 6px';
        jsonButton.style.marginLeft = '5px';
        jsonButton.onclick = exportToJson;
        exportContainer.appendChild(jsonButton);

        // 手动保存按钮
        const saveButton = document.createElement('button');
        saveButton.textContent = '手动保存';
        saveButton.style.fontSize = '12px';
        saveButton.style.padding = '3px 6px';
        saveButton.style.marginLeft = '5px';
        saveButton.onclick = function() {
            autoSaveData();
        };
        exportContainer.appendChild(saveButton);

        advancedContent.appendChild(exportContainer);
        advancedOptions.appendChild(advancedContent);
        contentContainer.appendChild(advancedOptions);

        container.appendChild(contentContainer);

        // 创建展开按钮(初始隐藏)
        const expandButton = document.createElement('div');
        expandButton.className = 'expand-button';
        expandButton.textContent = '+';
        expandButton.title = '展开';
        expandButton.style.display = 'none';
        expandButton.onclick = toggleMinimize;
        container.appendChild(expandButton);

        document.body.appendChild(container);

        // 创建状态指示器
        const indicator = document.createElement('div');
        indicator.className = 'status-indicator';
        indicator.id = 'status-indicator';
        indicator.style.display = 'none';
        document.body.appendChild(indicator);

        // 初始化内存统计
        updateMemoryStats();
    }

    // 切换最小化状态
    function toggleMinimize() {
        const container = document.querySelector('.auto-scroll-container');
        const contentContainer = container.querySelector('.content-container');
        const minimizeButton = container.querySelector('.minimize-button');
        const expandButton = container.querySelector('.expand-button');

        isMinimized = !isMinimized;

        if (isMinimized) {
            container.classList.add('minimized');
            contentContainer.style.display = 'none';
            minimizeButton.style.display = 'none';
            expandButton.style.display = 'flex';
        } else {
            container.classList.remove('minimized');
            contentContainer.style.display = 'block';
            minimizeButton.style.display = 'block';
            expandButton.style.display = 'none';
        }
    }

    // 更新状态指示器
    const updateStatus = debounce(function(message) {
        const indicator = document.getElementById('status-indicator');
        const statsDiv = document.getElementById('data-stats');

        if (!indicator || !statsDiv) return;

        statsDiv.textContent = `已采集 ${getTotalDataCount()} 条数据`;
        indicator.textContent = message;
        indicator.style.display = 'block';

        // 5秒后自动隐藏指示器
        setTimeout(() => {
            if (indicator && !isScrolling) {
                indicator.style.opacity = '0';
                setTimeout(() => {
                    indicator.style.display = 'none';
                    indicator.style.opacity = '1';
                }, 300);
            }
        }, 5000);
    }, 300);

    // 更新进度条
    function updateProgressBar(percentage) {
        const progressBar = document.getElementById('scroll-progress-bar');
        if (progressBar) {
            progressBar.style.width = Math.min(100, Math.max(0, percentage)) + '%';
        }
    }

    // 更新滚动统计信息
    function updateScrollStats() {
        const statsDiv = document.getElementById('scroll-stats');
        if (!statsDiv) return;

        let runTime = '0:00';
        if (scrollStartTime) {
            const elapsedSeconds = Math.floor((Date.now() - scrollStartTime) / 1000);
            const minutes = Math.floor(elapsedSeconds / 60);
            const seconds = elapsedSeconds % 60;
            runTime = `${minutes}:${seconds.toString().padStart(2, '0')}`;
        }

        statsDiv.innerHTML = `滚动次数: ${scrollAttempts} | 滚动距离: ${totalScrollDistance}px | 运行时间: ${runTime}`;
    }

    // 更新导出进度
    function updateExportProgress(percentage) {
        const progressDiv = document.getElementById('export-progress');
        if (progressDiv) {
            progressDiv.textContent = `导出进度: ${Math.round(percentage)}%`;
            progressDiv.style.display = 'block';

            if (percentage >= 100) {
                setTimeout(() => {
                    progressDiv.style.display = 'none';
                }, 3000);
            }
        }
    }

    // 初始化数据保存功能
    function initDataPreservation() {
        // 使用MutationObserver监视DOM变化
        observer = new MutationObserver(throttle(function(mutations) {
            if (!isScrolling || isPaused) return;

            // 查找ReactVirtualized组件
            const virtualList = document.querySelector('.ReactVirtualized__Grid__innerScrollContainer');
            if (!virtualList) return;

            // 获取当前显示的所有行
            const currentRows = Array.from(virtualList.children);
            if (currentRows.length === 0) return;

            let newDataAdded = false;
            let initialDataLength = allPreservedData.length;
            let newRowsData = [];

            // 提取每行的数据
            currentRows.forEach(row => {
                // 使用行的属性或内容创建唯一ID
                const rowId = row.getAttribute('aria-rowindex') ||
                              row.textContent.replace(/\s+/g, '').substring(0, 50);

                // 如果已处理过此行,则跳过
                if (processedRowIds.has(rowId)) return;

                // 标记为已处理
                processedRowIds.add(rowId);

                const playerElements = row.querySelectorAll('a');
                if (playerElements.length === 0) return;

                // 提取玩家数据
                const players = Array.from(playerElements)
                    .map(el => el.textContent.trim())
                    .filter(text => text);

                // 如果没有有效数据,则跳过
                if (players.length === 0) return;

                // 创建行数据对象
                const rowData = {
                    id: rowId,
                    timestamp: new Date().toISOString(),
                    players: players
                };

                // 添加到临时数组
                newRowsData.push(rowData);
                newDataAdded = true;
            });

            // 批量处理新数据
            if (newRowsData.length > 0) {
                // 如果启用内存优化,则只保留必要的数据
                if (memoryOptimization) {
                    newRowsData = newRowsData.map(row => ({
                        id: row.id,
                        players: row.players
                    }));
                }

                // 添加到保存列表
                allPreservedData = allPreservedData.concat(newRowsData);
                totalDataCollected += newRowsData.length;

                // 检查是否需要优化内存使用
                optimizeMemoryUsage();
            }

            // 检查是否有新数据添加
            if (newDataAdded) {
                noNewDataCounter = 0; // 重置计数器
                updateStatus(`正在滚动,已采集 ${getTotalDataCount()} 条数据`);

                // 更新进度条,动态调整估计总数
                const estimatedTotal = Math.max(1000, getTotalDataCount() * 2);
                const percentage = Math.min(100, Math.round((getTotalDataCount() / estimatedTotal) * 100));
                updateProgressBar(percentage);
            } else {
                noNewDataCounter++;

                // 如果连续多次没有新数据,可能已经到达底部
                if (noNewDataCounter >= autoStopThreshold) {
                    stopAutoScroll();
                    updateStatus(`已自动停止滚动,共采集 ${getTotalDataCount()} 条数据`);
                    document.querySelector('button[disabled]').disabled = false;
                    document.querySelectorAll('button')[0].disabled = true;
                    document.querySelectorAll('button')[1].disabled = true;
                }
            }
        }, 200));

        // 开始观察整个文档的变化
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    // 开始自动滚动
    function startAutoScroll() {
        if (isScrolling) return;

        isScrolling = true;
        isPaused = false;
        scrollAttempts = 0;
        noNewDataCounter = 0;
        samePositionCounter = 0;
        lastScrollPosition = window.scrollY;
        totalScrollDistance = 0;
        scrollStartTime = Date.now();
        lastAutoSaveTime = new Date();

        // 滚动到页面顶部
        window.scrollTo(0, 0);

        // 设置滚动间隔
        startScrollInterval();
    }

    // 开始滚动间隔
    function startScrollInterval() {
        if (scrollInterval) {
            clearInterval(scrollInterval);
        }

        scrollInterval = setInterval(() => {
            // 计算滚动步长,基于滚动速度
            const scrollStep = 100 * scrollSpeed;

            // 滚动页面
            window.scrollBy(0, scrollStep);
            totalScrollDistance += scrollStep;

            // 增加尝试次数
            scrollAttempts++;

            // 更新滚动统计信息
            updateScrollStats();

            // 检查是否已经到达页面底部
            const currentPosition = window.scrollY;
            if (Math.abs(currentPosition - lastScrollPosition) < 5) {
                samePositionCounter++;

                // 如果连续多次位置相同,判断为无法继续滚动
                if (samePositionCounter >= samePositionThreshold) {
                    stopAutoScroll();
                    updateStatus(`已到达页面底部,无法继续滚动,共采集 ${getTotalDataCount()} 条数据`);
                    document.querySelector('button[disabled]').disabled = false;
                    document.querySelectorAll('button')[0].disabled = true;
                    document.querySelectorAll('button')[1].disabled = true;
                    return;
                }
            } else {
                samePositionCounter = 0;
                lastScrollPosition = currentPosition;
            }

            // 如果达到最大尝试次数,停止滚动
            if (scrollAttempts >= maxScrollAttempts) {
                stopAutoScroll();
                updateStatus(`已达到最大滚动次数,共采集 ${getTotalDataCount()} 条数据`);
                document.querySelector('button[disabled]').disabled = false;
                document.querySelectorAll('button')[0].disabled = true;
                document.querySelectorAll('button')[1].disabled = true;
            }

            // 检查是否已经到达页面底部
            if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 200) {
                // 等待一段时间,看是否有新内容加载
                setTimeout(() => {
                    if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 200) {
                        noNewDataCounter++;

                        // 如果连续多次检测到底部,停止滚动
                        if (noNewDataCounter >= autoStopThreshold) {
                            stopAutoScroll();
                            updateStatus(`已到达页面底部,共采集 ${getTotalDataCount()} 条数据`);
                            document.querySelector('button[disabled]').disabled = false;
                            document.querySelectorAll('button')[0].disabled = true;
                            document.querySelectorAll('button')[1].disabled = true;
                        }
                    }
                }, pauseBeforeScroll);
            }
        }, 500 / scrollSpeed); // 滚动间隔随速度调整
    }

    // 停止自动滚动
    function stopAutoScroll() {
        if (!isScrolling) return;

        isScrolling = false;
        isPaused = false;
        if (scrollInterval) {
            clearInterval(scrollInterval);
            scrollInterval = null;
        }

        // 最后更新一次滚动统计信息
        updateScrollStats();
    }

    // 复制所有数据到剪贴板
    function copyAllData() {
        const totalCount = getTotalDataCount();
        if (totalCount === 0) {
            alert('没有采集的数据可复制');
            return;
        }

        // 如果数据量很大,提示用户
        if (totalCount > 1000) {
            if (!confirm(`数据量较大(${totalCount}条),复制可能需要一些时间,是否继续?`)) {
                return;
            }
        }

        // 获取所有数据
        const allData = getAllData();

        // 格式化数据为易读的文本
        let formattedData = '';

        // 按行组织数据
        const rowsMap = new Map();

        updateExportProgress(0);

        // 使用Web Worker处理大量数据
        const workerCode = `
            self.onmessage = function(e) {
                const data = e.data;
                const rowsMap = new Map();

                for (let i = 0; i < data.length; i++) {
                    const rowData = data[i];
                    // 使用第一个玩家名称作为行标识
                    const rowKey = rowData.players[0] || 'unknown';

                    if (!rowsMap.has(rowKey)) {
                        rowsMap.set(rowKey, rowData.players);
                    }

                    // 每处理100条数据,报告进度
                    if (i % 100 === 0) {
                        self.postMessage({
                            type: 'progress',
                            progress: Math.round((i / data.length) * 100)
                        });
                    }
                }

                // 将Map转换为数组并格式化输出
                let formattedData = '';
                let rowIndex = 1;
                rowsMap.forEach((players, key) => {
                    formattedData += '对局 ' + rowIndex++ + ':\\n';
                    players.forEach(player => {
                        formattedData += player + '\\n';
                    });
                    formattedData += '\\n';
                });

                self.postMessage({
                    type: 'result',
                    formattedData: formattedData,
                    rowCount: rowsMap.size
                });
            };
        `;

        const blob = new Blob([workerCode], { type: 'application/javascript' });
        const worker = new Worker(URL.createObjectURL(blob));

        worker.onmessage = function(e) {
            const message = e.data;

            if (message.type === 'progress') {
                updateExportProgress(message.progress);
            } else if (message.type === 'result') {
                // 创建临时文本区域并复制
                const textarea = document.createElement('textarea');
                textarea.value = message.formattedData;
                document.body.appendChild(textarea);
                textarea.select();
                document.execCommand('copy');
                document.body.removeChild(textarea);

                updateExportProgress(100);
                alert('已复制 ' + message.rowCount + ' 条数据到剪贴板');

                // 终止Worker
                worker.terminate();
            }
        };

        // 启动Worker处理数据
        worker.postMessage(allData);
    }

    // 导出为CSV格式
    function exportToCsv() {
        const totalCount = getTotalDataCount();
        if (totalCount === 0) {
            alert('没有采集的数据可导出');
            return;
        }

        // 获取所有数据
        const allData = getAllData();

        updateExportProgress(0);

        // 使用Web Worker处理大量数据
        const workerCode = `
            self.onmessage = function(e) {
                const data = e.data;
                const rowsMap = new Map();

                for (let i = 0; i < data.length; i++) {
                    const rowData = data[i];
                    // 使用第一个玩家名称作为行标识
                    const rowKey = rowData.players[0] || 'unknown';

                    if (!rowsMap.has(rowKey)) {
                        rowsMap.set(rowKey, rowData.players);
                    }

                    // 每处理100条数据,报告进度
                    if (i % 100 === 0) {
                        self.postMessage({
                            type: 'progress',
                            progress: Math.round((i / data.length) * 50) // 前半部分进度
                        });
                    }
                }

                // 找出最大玩家数量
                let maxPlayers = 0;
                rowsMap.forEach(players => {
                    maxPlayers = Math.max(maxPlayers, players.length);
                });

                // 创建CSV内容
                let csvContent = '';

                // 添加表头
                let headers = ['对局'];
                for (let i = 1; i <= maxPlayers; i++) {
                    headers.push('玩家' + i);
                }
                csvContent += headers.join(',') + '\\r\\n';

                // 添加数据行
                let rowIndex = 1;
                let processedRows = 0;
                const totalRows = rowsMap.size;

                rowsMap.forEach((players, key) => {
                    let row = ['对局' + rowIndex++];
                    players.forEach(player => {
                        // 处理CSV中的特殊字符
                        row.push('"' + player.replace(/"/g, '""') + '"');
                    });

                    // 补齐空列
                    while (row.length <= maxPlayers) {
                        row.push('');
                    }

                    csvContent += row.join(',') + '\\r\\n';

                    // 更新后半部分进度
                    processedRows++;
                    if (processedRows % 10 === 0) {
                        self.postMessage({
                            type: 'progress',
                            progress: 50 + Math.round((processedRows / totalRows) * 50)
                        });
                    }
                });

                self.postMessage({
                    type: 'result',
                    csvContent: csvContent,
                    rowCount: rowsMap.size
                });
            };
        `;

        const blob = new Blob([workerCode], { type: 'application/javascript' });
        const worker = new Worker(URL.createObjectURL(blob));

        worker.onmessage = function(e) {
            const message = e.data;

            if (message.type === 'progress') {
                updateExportProgress(message.progress);
            } else if (message.type === 'result') {
                // 创建下载链接
                const encodedUri = encodeURI('data:text/csv;charset=utf-8,' + message.csvContent);
                const link = document.createElement('a');
                link.setAttribute('href', encodedUri);
                link.setAttribute('download', '雀魂牌谱数据_' + new Date().toISOString().slice(0,10) + '.csv');
                document.body.appendChild(link);

                // 触发下载
                link.click();
                document.body.removeChild(link);

                updateExportProgress(100);
                alert('已导出 ' + message.rowCount + ' 条数据为CSV文件');

                // 终止Worker
                worker.terminate();
            }
        };

        // 启动Worker处理数据
        worker.postMessage(allData);
    }

    // 导出为JSON格式
    function exportToJson() {
        const totalCount = getTotalDataCount();
        if (totalCount === 0) {
            alert('没有采集的数据可导出');
            return;
        }

        // 获取所有数据
        const allData = getAllData();

        updateExportProgress(0);

        // 使用Web Worker处理大量数据
        const workerCode = `
            self.onmessage = function(e) {
                const data = e.data;
                const rowsMap = new Map();

                for (let i = 0; i < data.length; i++) {
                    const rowData = data[i];
                    // 使用第一个玩家名称作为行标识
                    const rowKey = rowData.players[0] || 'unknown';

                    if (!rowsMap.has(rowKey)) {
                        rowsMap.set(rowKey, rowData.players);
                    }

                    // 每处理100条数据,报告进度
                    if (i % 100 === 0) {
                        self.postMessage({
                            type: 'progress',
                            progress: Math.round((i / data.length) * 50) // 前半部分进度
                        });
                    }
                }

                // 转换为JSON数组
                const jsonData = [];
                let rowIndex = 1;
                let processedRows = 0;
                const totalRows = rowsMap.size;

                rowsMap.forEach((players, key) => {
                    jsonData.push({
                        id: rowIndex++,
                        players: players
                    });

                    // 更新后半部分进度
                    processedRows++;
                    if (processedRows % 10 === 0) {
                        self.postMessage({
                            type: 'progress',
                            progress: 50 + Math.round((processedRows / totalRows) * 50)
                        });
                    }
                });

                // 创建JSON内容
                const jsonContent = JSON.stringify(jsonData, null, 2);

                self.postMessage({
                    type: 'result',
                    jsonContent: jsonContent,
                    rowCount: jsonData.length
                });
            };
        `;

        const blob = new Blob([workerCode], { type: 'application/javascript' });
        const worker = new Worker(URL.createObjectURL(blob));

        worker.onmessage = function(e) {
            const message = e.data;

            if (message.type === 'progress') {
                updateExportProgress(message.progress);
            } else if (message.type === 'result') {
                // 创建下载链接
                const jsonContent = 'data:text/json;charset=utf-8,' +
                                   encodeURIComponent(message.jsonContent);

                const link = document.createElement('a');
                link.setAttribute('href', jsonContent);
                link.setAttribute('download', '雀魂牌谱数据_' + new Date().toISOString().slice(0,10) + '.json');
                document.body.appendChild(link);

                // 触发下载
                link.click();
                document.body.removeChild(link);

                updateExportProgress(100);
                alert('已导出 ' + message.rowCount + ' 条数据为JSON文件');

                // 终止Worker
                worker.terminate();
            }
        };

        // 启动Worker处理数据
        worker.postMessage(allData);
    }

    // 在页面关闭前提示用户保存数据
    window.addEventListener('beforeunload', function(e) {
        if (getTotalDataCount() > 0) {
            e.preventDefault();
            e.returnValue = '页面上有未保存的数据,确定要离开吗?';
            return e.returnValue;
        }
    });
})();