鱼糕V2 钓饵统计器

智能实时统计鱼糕V2钓饵,统一元素等待机制,修复 ReferenceError。

// ==UserScript==
// @name    鱼糕V2 钓饵统计器
// @version  3.6
// @description 智能实时统计鱼糕V2钓饵,统一元素等待机制,修复 ReferenceError。
// @author   Atail@神意之地
// @match    https://fish.ffmomola.com/ng/
// @icon     https://www.google.com/s2/favicons?sz=64@domain=ffmomola.com
// @grant    GM_notification
// @namespace https://gf.qytechs.cn/users/437453
// ==/UserScript==

(() => {
    'use strict';

    const fishingRecordsPageUrl = 'https://fish.ffmomola.com/ng/#/fishing';
    const tableBodySelector = 'tbody.MuiTableBody-root';
    const fishingRecordRowSelector = 'tr.MuiTableRow-root:not(.MuiTableRow-head)';
    const baitInfoCellIndex = 5;
    const baitLabelContainerSelector = 'div.MuiAvatar-root[aria-label]';

    // 钓饵统计Set
    let uniqueBaitNames = new Set();
    // 已处理的行Set,用于跟踪已处理的DOM行元素
    let processedRows = new Set();

    let tableBodyObserverInstance = null;
    let isTableBodyObserverSetup = false;

    // --- 辅助函数 ---
    const createNotification = (bait) => GM_notification({
        text: `[鱼糕V2] 钓饵 "${bait}" 已复制到剪贴板`,
        title: '提示',
        image: 'https://www.google.com/s2/favicons?sz=64@domain=ffmomola.com',
        timeout: 2000
    });

    /**
     * 处理单个钓鱼记录行,提取钓饵信息并添加到 uniqueBaitNames Set
     * @param {Element} recordRow 钓鱼记录的DOM行元素
     * @returns {boolean} 如果是新处理的行返回true,否则返回false
     */
    const processFishingRecordRow = (recordRow) => {
        if (processedRows.has(recordRow)) {
            return false;
        }
        processedRows.add(recordRow);

        const baitCell = recordRow.children[baitInfoCellIndex];
        if (baitCell) {
            const baitLabelContainer = baitCell.querySelector(baitLabelContainerSelector);
            if (baitLabelContainer) {
                const baitName = baitLabelContainer.getAttribute('aria-label');
                if (baitName && baitName !== '捕鱼人之识' && baitName !== '钓组') {
                    uniqueBaitNames.add(baitName);
                }
            }
        }
        return true;
    };

    /**
     * 扫描并显示钓饵统计
     * @param {boolean} clearBeforeScan 是否在扫描前清空所有现有统计数据
     */
    const scanAndDisplayBaitSummary = (clearBeforeScan = false) => {
        console.log(`[鱼糕V2] 正在扫描并显示钓饵统计 (清空模式: ${clearBeforeScan})`);

        const tableBodyElement = document.querySelector(tableBodySelector);
        if (!tableBodyElement) {
            console.warn("[鱼糕V2] 扫描时未找到 tbody.MuiTableBody-root 元素。");
            return;
        }

        if (clearBeforeScan) {
            console.log('[鱼糕V2] 清空现有统计数据以进行全新扫描。');
            uniqueBaitNames.clear();
            processedRows.clear();
        }

        const fishingRecordRows = tableBodyElement.querySelectorAll(fishingRecordRowSelector);
        let newRowsProcessedCount = 0;
        fishingRecordRows.forEach(row => {
            if (processFishingRecordRow(row)) {
                newRowsProcessedCount++;
            }
        });
        console.log(`[鱼糕V2] 本次扫描处理了 ${newRowsProcessedCount} 条新行。`);
        console.log(`[鱼糕V2] 当前总计找到 ${uniqueBaitNames.size} 种不同的钓饵。`);

        const targetContainerElement = tableBodyElement.closest('div.MuiPaper-root') || tableBodyElement.parentNode;
        if (!targetContainerElement) {
            console.error("[鱼糕V2] 未找到合适的插入目标容器,无法显示钓饵统计。");
            return;
        }

        const previousBaitTagsContainer = document.querySelector('div[data-script-version="鱼糕V2钓饵统计"]');
        if (previousBaitTagsContainer) {
            previousBaitTagsContainer.remove();
            console.log('[鱼糕V2] 已移除旧的钓饵统计容器。');
        }

        const baitTagsContainer = document.createElement('div');
        baitTagsContainer.style.cssText = 'display: flex; flex-wrap: wrap; gap: 4px; padding: 8px';
        const fragment = document.createDocumentFragment();

        if (uniqueBaitNames.size === 0) {
            const noBaitTag = document.createElement('div');
            noBaitTag.textContent = "当前无钓饵数据";
            noBaitTag.style.cssText = `
                font-size: 14px;
                color: #aaa;
                padding: 0 12px;
            `;
            fragment.appendChild(noBaitTag);
        } else {
            const sortedBaitNames = Array.from(uniqueBaitNames).sort((a, b) => a.localeCompare(b, 'zh-Hans-CN'));
            sortedBaitNames.forEach(bait => {
                const baitTagElement = document.createElement('div');
                baitTagElement.textContent = bait;
                baitTagElement.style.cssText = `
                    border-radius: 16px;
                    font-size: 14px;
                    height: 32px;
                    line-height: 32px;
                    color: #fff;
                    padding: 0 12px;
                    border: 1px solid #ffffff1f;
                    user-select: none;
                    background-color: #333333;
                    cursor: pointer;
                    transition: background-color 0.1s ease-in-out;
                `;
                baitTagElement.addEventListener('click', () => {
                    navigator.clipboard.writeText(bait).then(() => {
                        createNotification(bait);
                        const originalBackgroundColor = baitTagElement.style.backgroundColor;
                        baitTagElement.style.backgroundColor = '#555555';
                        setTimeout(() => {
                            baitTagElement.style.backgroundColor = originalBackgroundColor;
                        }, 500);
                    }).catch(err => {
                        console.error('[鱼糕V2] 复制到剪贴板失败: ', err);
                    });
                });
                fragment.appendChild(baitTagElement);
            });
        }

        baitTagsContainer.appendChild(fragment);
        baitTagsContainer.dataset.scriptVersion = '鱼糕V2钓饵统计';
        targetContainerElement.parentNode.insertBefore(baitTagsContainer, targetContainerElement);
        console.log('[鱼糕V2] 钓饵统计显示完毕。');
    };

    /**
     * 启动 tbody 的 MutationObserver,监听筛选操作
     */
    const setupTableBodyObserverForFilters = () => {
        const tableBodyElement = document.querySelector(tableBodySelector);
        if (!tableBodyElement) {
            console.error("[鱼糕V2] 尝试设置 tbody 观察者时未找到 tbody 元素。");
            return;
        }
        if (isTableBodyObserverSetup) {
            console.log('[鱼糕V2] tbody 观察者已设置,跳过重复设置。');
            return;
        }

        tableBodyObserverInstance = new MutationObserver((mutationsList, observer) => {
            // 在 MutationObserver 回调中,延迟执行,等待 DOM 稳定
            setTimeout(() => {
                // 此时用户执行了筛选,需要清空并重新扫描所有当前显示的行
                console.log('[鱼糕V2] 检测到 tbody 内容变化 (可能是筛选),正在重新统计...');
                scanAndDisplayBaitSummary(true); // 强制清空并重新统计
            }, 100); // 稍微长一点的延迟,确保筛选后的 DOM 渲染稳定
        });

        tableBodyObserverInstance.observe(tableBodyElement, { childList: true });
        console.log(`[鱼糕V2] 成功启动 ${tableBodySelector} 的 MutationObserver,监听用户筛选操作。`);
        isTableBodyObserverSetup = true;
    };

    /**
     * 等待指定选择器元素出现,并可选择匹配文本内容
     * @param {string} selector 要等待的元素选择器 (例如 'tbody.MuiTableBody-root' 或 'button')
     * @param {number} timeout 最长等待时间(毫秒)
     * @param {string} [textContentMatch=''] 如果提供,元素必须包含此文本内容
     * @returns {Promise<Element|null>} 找到的元素或 null(超时)
     */
    const waitForElement = (selector, timeout = 5000, textContentMatch = '') => {
        return new Promise(resolve => {
            const startTime = Date.now();
            const checkElement = () => {
                const elements = document.querySelectorAll(selector);
                let foundElement = null;

                for (const element of elements) {
                    // 确保元素存在且可见
                    if (element && element.offsetParent !== null) {
                        // 如果有文本内容要求,检查是否包含该文本
                        if (textContentMatch) {
                            if (element.textContent.includes(textContentMatch)) {
                                foundElement = element;
                                break;
                            }
                        } else {
                            // 没有文本内容要求,只要找到可见元素即可
                            foundElement = element;
                            break;
                        }
                    }
                }

                if (foundElement) {
                    resolve(foundElement);
                    return;
                }

                if (Date.now() - startTime > timeout) {
                    console.log(`[鱼糕V2] 等待元素 ${selector} (文本: "${textContentMatch}") 超时 (${timeout}ms)。`);
                    resolve(null); // 超时未找到
                    return;
                }
                setTimeout(checkElement, 100); // 每100ms检查一次
            };
            checkElement();
        });
    };

    /**
     * 循环点击“加载更多”按钮直到其消失
     */
    const clickLoadMoreUntilGone = async () => {
        return new Promise(resolve => {
            const clickLoop = async () => {
                // 使用通用的 waitForElement 查找“加载更多”按钮
                const loadMoreButton = await waitForElement('button.MuiButtonBase-root, button.MuiButton-root', 1000, '加载更多');
                if (loadMoreButton) { // 检查按钮是否存在且可见
                    console.log('[鱼糕V2] 发现“加载更多”按钮,正在点击...');
                    loadMoreButton.click();
                    // 在点击后,等待一段时间让数据加载,然后递归调用
                    setTimeout(clickLoop, 500); // 0.5秒后再次检查
                } else {
                    console.log('[鱼糕V2] “加载更多”按钮未找到或已消失,所有记录可能已加载。');
                    resolve(); // 按钮消失,Promise 完成
                }
            };
            clickLoop();
        });
    };


    /**
     * 核心启动函数:按照逻辑顺序执行
     */
    const initializeAndProcessRecords = async () => {
        console.log('[鱼糕V2] 脚本核心流程启动。');

        // 第一步:等待 tbody.MuiTableBody-root 出现
        console.log('[鱼糕V2] 等待 tbody.MuiTableBody-root 元素出现...');
        // 使用通用 waitForElement,无需文本匹配
        const tableBodyElement = await waitForElement(tableBodySelector, 10000); // 最长等待10秒
        if (!tableBodyElement) {
            console.error("[鱼糕V2] 致命错误:tbody.MuiTableBody-root 元素在超时时间内未出现,脚本终止。");
            return;
        }
        console.log('[鱼糕V2] tbody.MuiTableBody-root 已出现。');

        // 第二步:尝试查找并点击“加载更多”按钮
        console.log('[鱼糕V2] 尝试查找“加载更多”按钮...');
        // 第一次查找,给足够时间让按钮刷新出来,并指定文本内容
        const initialLoadMoreButton = await waitForElement('button.MuiButtonBase-root, button.MuiButton-root', 5000, '加载更多'); // 给5秒时间查找初始按钮

        if (initialLoadMoreButton) {
            console.log('[鱼糕V2] 发现“加载更多”按钮,开始自动加载所有记录...');
            await clickLoadMoreUntilGone();
            console.log('[鱼糕V2] “加载更多”流程完成。');
        } else {
            console.log('[鱼糕V2] 未发现“加载更多”按钮,假定数据已全部加载或无更多数据。');
        }

        // 第三步:所有记录加载完毕(或无加载更多),执行第一次完整的钓饵统计
        console.log('[鱼糕V2] 执行首次完整钓饵统计。');
        scanAndDisplayBaitSummary(true); // 首次统计,清空并扫描所有当前已加载的行

        // 第四步:启动 tbody 的 MutationObserver,监听后续的筛选操作
        setupTableBodyObserverForFilters();
    };

    // --- 脚本入口 ---
    const startScript = () => {
        console.log('[鱼糕V2] 脚本入口开始');
        if (window.location.href.startsWith(fishingRecordsPageUrl)) {
            console.log('[鱼糕V2] 当前 URL 与钓鱼记录页面 URL 匹配。');
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', initializeAndProcessRecords);
            } else {
                initializeAndProcessRecords();
            }
        } else {
            console.log(`[鱼糕V2] 当前 URL (${window.location.href}) 不是钓鱼记录页面 URL (${fishingRecordsPageUrl}),脚本将不会执行。`);
        }
    };

    startScript();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址