Metrics Extractor for Xiaohongshu Note Manager

聪明豆专属!!提取并展示指标数据(浏览量、评论数、点赞数、收藏数、转发数),并自动更新以应对动态加载的数据。

当前为 2024-12-13 提交的版本,查看 最新版本

// ==UserScript==
// @name         Metrics Extractor for Xiaohongshu Note Manager
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  聪明豆专属!!提取并展示指标数据(浏览量、评论数、点赞数、收藏数、转发数),并自动更新以应对动态加载的数据。
// @author       您的名字
// @match        https://creator.xiaohongshu.com/new/note-manager*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // 防止脚本多次运行
    if (document.getElementById('metrics-extractor-container')) {
        console.log('Metrics Extractor 已经运行,退出脚本。');
        return;
    }

    const metrics = ['浏览量', '评论数', '点赞数', '收藏数', '转发数'];
    let categorizedData = [];
    let totals = {
        '浏览量': 0,
        '评论数': 0,
        '点赞数': 0,
        '收藏数': 0,
        '转发数': 0
    };

    /**
     * 提取指标数据
     * @param {HTMLElement} div - 包含数字的 div.icon 元素
     * @returns {number|null} 提取的数字,或 null 失败
     */
    function extractNumberFromDiv(div, index) {
        // 使用 :scope > span 选择器,确保选择 div.icon 的直接子 span
        const span = div.querySelector(':scope > span');
        if (span) {
            // 使用 innerText 代替 textContent
            const numberText = span.innerText.trim();
            console.log(`div.icon #${index + 1} - 提取到的span文本: "${numberText}"`);
            
            // 检查是否有数字
            const digitMatch = numberText.match(/\d+/g);
            if (digitMatch) {
                // 将所有找到的数字部分连接起来
                const cleanedNumber = digitMatch.join('');
                console.log(`div.icon #${index + 1} - 清理后的数字字符串: "${cleanedNumber}"`);
                
                const number = parseInt(cleanedNumber, 10);
                if (!isNaN(number)) {
                    console.log(`div.icon #${index + 1} - 解析后的数字: ${number}`);
                    return number;
                } else {
                    console.warn(`div.icon #${index + 1} - 无法解析数字: "${cleanedNumber}"`);
                    return null;
                }
            } else {
                console.warn(`div.icon #${index + 1} - span内未找到数字`);
                return null;
            }
        } else {
            console.warn(`div.icon #${index + 1} - 未找到span元素`);
            return null;
        }
    }

    /**
     * 更新指标总数和详细数据
     * @param {number} number - 提取的数字
     * @param {number} metricIndex - 指标索引(0-4)
     */
    function updateMetrics(number, metricIndex) {
        const metric = metrics[metricIndex];
        if (metric) {
            totals[metric] += number;

            // 更新详细数据
            let currentItem = categorizedData[categorizedData.length - 1];
            if (!currentItem || Object.keys(currentItem).length >= metrics.length) {
                currentItem = {};
                categorizedData.push(currentItem);
            }
            currentItem[metric] = number;
        }
    }

    /**
     * 创建指标展示面板
     */
    function createMetricsPanel() {
        console.log('创建指标面板...');
        // 创建样式
        const style = document.createElement('style');
        style.innerHTML = `
        #metrics-extractor-container {
            position: fixed;
            top: 20px;
            right: 20px;
            width: 350px;
            background: rgba(255, 255, 255, 0.95);
            border: 1px solid #ccc;
            border-radius: 8px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.2);
            padding: 15px;
            z-index: 10000;
            font-family: Arial, sans-serif;
            max-height: 90vh;
            overflow-y: auto;
        }
        #metrics-extractor-container h2 {
            text-align: center;
            margin-top: 0;
            font-size: 18px;
        }
        #metrics-extractor-container table {
            width: 100%;
            border-collapse: collapse;
            margin-bottom: 15px;
        }
        #metrics-extractor-container th, #metrics-extractor-container td {
            border: 1px solid #ddd;
            padding: 8px;
            text-align: center;
            font-size: 14px;
        }
        #metrics-extractor-container th {
            background-color: #f2f2f2;
        }
        #metrics-extractor-container button {
            width: 100%;
            padding: 10px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            margin-bottom: 10px;
        }
        #metrics-extractor-container button:hover {
            background-color: #45a049;
        }
        #metrics-extractor-details {
            display: none;
            max-height: 300px;
            overflow-y: auto;
        }
        `;
        document.head.appendChild(style);

        // 创建容器
        const container = document.createElement('div');
        container.id = 'metrics-extractor-container';

        // 创建标题
        const title = document.createElement('h2');
        title.textContent = '指标总数';
        container.appendChild(title);

        // 创建总数表格
        const totalsTable = document.createElement('table');
        const totalsThead = document.createElement('thead');
        const totalsHeaderRow = document.createElement('tr');
        const metricHeader = document.createElement('th');
        metricHeader.textContent = '指标';
        const totalHeader = document.createElement('th');
        totalHeader.textContent = '总数';
        totalsHeaderRow.appendChild(metricHeader);
        totalsHeaderRow.appendChild(totalHeader);
        totalsThead.appendChild(totalsHeaderRow);
        totalsTable.appendChild(totalsThead);

        const totalsTbody = document.createElement('tbody');
        for (const [metric, total] of Object.entries(totals)) {
            const row = document.createElement('tr');
            const metricCell = document.createElement('td');
            metricCell.textContent = metric;
            const totalCell = document.createElement('td');
            totalCell.textContent = total;
            row.appendChild(metricCell);
            row.appendChild(totalCell);
            totalsTbody.appendChild(row);
        }
        totalsTable.appendChild(totalsTbody);
        container.appendChild(totalsTable);

        // 创建按钮
        const toggleButton = document.createElement('button');
        toggleButton.id = 'metrics-extractor-toggle';
        toggleButton.textContent = '显示详细数据';
        container.appendChild(toggleButton);

        // 创建详细数据部分
        const detailsSection = document.createElement('div');
        detailsSection.id = 'metrics-extractor-details';

        const detailsTitle = document.createElement('h2');
        detailsTitle.textContent = '详细数据';
        detailsSection.appendChild(detailsTitle);

        const detailsTable = document.createElement('table');
        const detailsThead = document.createElement('thead');
        const detailsHeaderRow = document.createElement('tr');
        const projectHeader = document.createElement('th');
        projectHeader.textContent = '项目';
        detailsHeaderRow.appendChild(projectHeader);
        metrics.forEach(metric => {
            const th = document.createElement('th');
            th.textContent = metric;
            detailsHeaderRow.appendChild(th);
        });
        detailsThead.appendChild(detailsHeaderRow);
        detailsTable.appendChild(detailsThead);

        const detailsTbody = document.createElement('tbody');
        detailsSection.appendChild(detailsTable);
        detailsSection.appendChild(detailsTbody);
        container.appendChild(detailsSection);
        document.body.appendChild(container);

        // 添加按钮点击事件
        toggleButton.addEventListener('click', () => {
            if (detailsSection.style.display === 'none' || detailsSection.style.display === '') {
                detailsSection.style.display = 'block';
                toggleButton.textContent = '隐藏详细数据';
            } else {
                detailsSection.style.display = 'none';
                toggleButton.textContent = '显示详细数据';
            }
        });

        console.log('指标面板已创建。');
    }

    /**
     * 更新指标展示面板的数据
     */
    function updateMetricsPanel() {
        console.log('更新指标面板数据...');
        const totalsTableBody = document.querySelector('#metrics-extractor-container table tbody');
        const detailsTableBody = document.querySelector('#metrics-extractor-details table tbody');

        // 更新总数表格
        totalsTableBody.innerHTML = '';
        for (const [metric, total] of Object.entries(totals)) {
            const row = document.createElement('tr');
            const metricCell = document.createElement('td');
            metricCell.textContent = metric;
            const totalCell = document.createElement('td');
            totalCell.textContent = total;
            row.appendChild(metricCell);
            row.appendChild(totalCell);
            totalsTableBody.appendChild(row);
        }

        // 更新详细数据表格
        detailsTableBody.innerHTML = '';
        categorizedData.forEach((item, index) => {
            const row = document.createElement('tr');
            const projectCell = document.createElement('td');
            projectCell.textContent = index + 1;
            row.appendChild(projectCell);
            metrics.forEach(metric => {
                const cell = document.createElement('td');
                cell.textContent = item[metric] !== null ? item[metric] : '-';
                row.appendChild(cell);
            });
            detailsTableBody.appendChild(row);
        });

        console.log('指标面板数据已更新。');
    }

    /**
     * 等待指定的条件满足
     * @param {Function} conditionFunction - 返回布尔值的函数,表示条件是否满足
     * @param {number} timeout - 最大等待时间(毫秒)
     * @param {number} interval - 检查间隔时间(毫秒)
     * @returns {Promise<boolean>} 条件是否在超时前满足
     */
    function waitFor(conditionFunction, timeout = 30000, interval = 500) { // 延长超时时间至30秒
        return new Promise((resolve) => {
            const startTime = Date.now();
            const checkCondition = () => {
                if (conditionFunction()) {
                    resolve(true);
                } else if (Date.now() - startTime >= timeout) {
                    resolve(false);
                } else {
                    setTimeout(checkCondition, interval);
                }
            };
            checkCondition();
        });
    }

    /**
     * 初始化脚本
     */
    async function init() {
        console.log('Metrics Extractor 脚本初始化...');
        // **调整等待条件**:等待 div.icon_list 内至少有 1 个 div.icon 元素,并且其 span 内有内容
        const elementsLoaded = await waitFor(() => {
            const icons = document.querySelectorAll('div.icon_list div.icon');
            if (icons.length >= 1) { // 根据实际情况调整
                return Array.from(icons).every(div => {
                    const span = div.querySelector(':scope > span');
                    return span && span.innerText.trim() !== '';
                });
            }
            return false;
        }, 30000, 500); // 延长超时时间至30秒

        if (!elementsLoaded) {
            console.warn('等待目标元素超时,未找到足够的div.icon元素或span内无内容。');
            return;
        }

        // 创建指标面板
        createMetricsPanel();

        // 提取初始数据
        const initialIcons = document.querySelectorAll('div.icon_list div.icon');
        initialIcons.forEach((div, index) => {
            if (!div.dataset.processed) { // 检查是否已处理
                const number = extractNumberFromDiv(div, index + 1);
                if (number !== null) {
                    updateMetrics(number, 0); // 假设每5个div.icon对应5个metrics
                }
                div.dataset.processed = 'true'; // 标记为已处理
            }
        });

        // 更新面板
        updateMetricsPanel();

        // 设置 MutationObserver 监听 icon_list 的变化
        const iconList = document.querySelector('div.icon_list');
        if (iconList) {
            const observer = new MutationObserver(mutations => {
                mutations.forEach(mutation => {
                    if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                        mutation.addedNodes.forEach(node => {
                            if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('icon')) {
                                if (!node.dataset.processed) { // 检查是否已处理
                                    const iconIndex = categorizedData.length * metrics.length + metrics.indexOf('浏览量') + 1;
                                    const number = extractNumberFromDiv(node, iconIndex);
                                    if (number !== null) {
                                        updateMetrics(number, 0); // 根据指标顺序调整 index
                                        updateMetricsPanel();
                                    }
                                    node.dataset.processed = 'true'; // 标记为已处理
                                }
                            }
                        });
                    }
                });
            });

            observer.observe(iconList, { childList: true, subtree: false });
            console.log('MutationObserver 已启动,监听 div.icon_list 的变化。');
        } else {
            console.warn('未找到 div.icon_list 元素,无法启动 MutationObserver。');
        }

        console.log('Metrics Extractor 脚本运行完毕。');
    }

    /**
     * 提取数字并返回
     * @param {HTMLElement} div - div.icon 元素
     * @param {number} index - 元素索引
     * @returns {number|null} 提取的数字或 null
     */
    function extractNumberFromDiv(div, index) {
        // 使用 :scope > span 选择器,确保选择 div.icon 的直接子 span
        const span = div.querySelector(':scope > span');
        if (span) {
            // 使用 innerText 代替 textContent
            const numberText = span.innerText.trim();
            console.log(`div.icon #${index} - 提取到的span文本: "${numberText}"`);
            
            // 检查是否有数字
            const digitMatch = numberText.match(/\d+/g);
            if (digitMatch) {
                // 将所有找到的数字部分连接起来
                const cleanedNumber = digitMatch.join('');
                console.log(`div.icon #${index} - 清理后的数字字符串: "${cleanedNumber}"`);
                
                const number = parseInt(cleanedNumber, 10);
                if (!isNaN(number)) {
                    console.log(`div.icon #${index} - 解析后的数字: ${number}`);
                    return number;
                } else {
                    console.warn(`div.icon #${index} - 无法解析数字: "${cleanedNumber}"`);
                    return null;
                }
            } else {
                console.warn(`div.icon #${index} - span内未找到数字`);
                return null;
            }
        } else {
            console.warn(`div.icon #${index} - 未找到span元素`);
            return null;
        }
    }

    /**
     * 更新指标总数和详细数据
     * @param {number} number - 提取的数字
     * @param {number} metricIndex - 指标索引(0-4)
     */
    function updateMetrics(number, metricIndex) {
        const metric = metrics[metricIndex];
        if (metric) {
            totals[metric] += number;

            // 更新详细数据
            let currentItem = categorizedData[categorizedData.length - 1];
            if (!currentItem || Object.keys(currentItem).length >= metrics.length) {
                currentItem = {};
                categorizedData.push(currentItem);
            }
            currentItem[metric] = number;
        }
    }

    /**
     * 创建指标展示面板
     */
    function createMetricsPanel() {
        // 创建面板和相关元素的逻辑
        // 已在 init 函数中实现
    }

    // 运行初始化函数
    init();

})();