Coupang 评论助手

在Coupang商品详情页定位指定reviewId的评论,滚动并高亮。新增功能:1. 在侧边面板显示所有评论ID并支持点击跳转。 2. 快速查找并定位带有"TOP"标识的优质评论。

// ==UserScript==
// @name         Coupang 评论助手
// @name:zh-CN   Coupang 评论助手
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  在Coupang商品详情页定位指定reviewId的评论,滚动并高亮。新增功能:1. 在侧边面板显示所有评论ID并支持点击跳转。 2. 快速查找并定位带有"TOP"标识的优质评论。
// @description:zh-CN 在Coupang商品详情页定位指定reviewId的评论,滚动并高亮。新增功能:1. 在侧边面板显示所有评论ID并支持点击跳转。 2. 快速查找并定位带有"TOP"标识的优质评论。
// @license      MIT
// @author       nobody
// @match        https://www.coupang.com/vp/products/*
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    console.log('[Coupang Review Finder] 脚本运行中...');

    const SINGLE_REVIEW_BLOCK_SELECTOR = 'article.sdp-review__article__list.js_reviewArticleReviewList';
    const REVIEW_ID_CONTAINER_SELECTOR = '.sdp-review__article__list__help'; // 这个元素通常包含data-review-id
    const REVIEW_ID_ATTRIBUTE = 'data-review-id';
    const HIGHLIGHT_CLASS = 'tm-review-highlight'; // 高亮样式类名
    const REVIEW_ID_LIST_PANEL_ID = 'tm-review-id-list-panel'; // ID 列表面板的ID
    const TOP_REVIEW_BADGE_SELECTOR = '.sdp-review__article__list__info__top-badge'; // Top评论徽章选择器

    let highlightTimeoutId = null; // 用于存储高亮setTimeout的ID
    let currentHighlightedElement = null; // 用于追踪当前高亮的元素(临时高亮结束后会清空)
    let topReviewElements = []; // 存储所有Top评论元素
    let currentTopReviewIndex = -1; // 当前高亮的Top评论索引

    // 注入CSS用于评论高亮和按钮/面板样式
    GM_addStyle(`
        .${HIGHLIGHT_CLASS} {
            border: 2px solid #007bff !important; /* 蓝色边框 */
            box-shadow: 0 0 10px rgba(0, 123, 255, 0.5) !important; /* 蓝色阴影 */
            transition: all 0.2s ease-in-out; /* 平滑过渡效果 */
        }
        /* 保持按钮美观的共同样式 */
        .tm-userscript-button {
            position: fixed;
            padding: 10px;
            background-color: #007bff;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            z-index: 99999;
            transition: background-color 0.2s ease;
            text-align: center;
        }
        .tm-userscript-button:hover {
            background-color: #0056b3;
        }
        .tm-userscript-button:active {
            box-shadow: 0 1px 3px rgba(0,0,0,0.2) inset;
        }

        /* 评论ID列表面板样式 */
        #${REVIEW_ID_LIST_PANEL_ID} {
            position: fixed;
            top: 160px; /* 在所有按钮下方 */
            left: 10px;
            z-index: 99998; /* 比按钮低一点 */
            width: 200px; /* 固定宽度 */
            max-height: 400px; /* 最大高度,允许滚动 */
            overflow-y: auto; /* 超出则垂直滚动 */
            background-color: #f9f9f9;
            border: 1px solid #ddd;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            padding: 10px;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            font-size: 13px;
            line-height: 1.6;
            color: #333;
            display: none; /* 默认隐藏 */
        }
        #${REVIEW_ID_LIST_PANEL_ID} .tm-review-id-item {
            display: block;
            padding: 6px 8px;
            margin-bottom: 4px;
            background-color: #e9ecef;
            border-radius: 4px;
            cursor: pointer;
            transition: background-color 0.2s ease, transform 0.1s ease;
            word-break: break-all; /* 防止长ID溢出 */
            text-decoration: none;
            color: #212529;
        }
        #${REVIEW_ID_LIST_PANEL_ID} .tm-review-id-item:hover {
            background-color: #dee2e6;
            transform: translateY(-1px);
        }
    `);

    /**
     * 在评论列表中查找指定 reviewId 的评论元素。
     * @param {string} reviewId 要查找的评论ID。
     * @returns {HTMLElement|null} 找到的评论元素,如果未找到则返回 null。
     */
    function findReviewElementById(reviewId) {
        const reviewBlocks = document.querySelectorAll(SINGLE_REVIEW_BLOCK_SELECTOR);
        console.log(`[Coupang Review Finder] 发现 ${reviewBlocks.length} 个评论块进行检查。`);

        for (let i = 0; i < reviewBlocks.length; i++) {
            const reviewBlock = reviewBlocks[i];
            let currentReviewId = reviewBlock.getAttribute(REVIEW_ID_ATTRIBUTE);
            if (!currentReviewId) {
                const idContainer = reviewBlock.querySelector(REVIEW_ID_CONTAINER_SELECTOR);
                if (idContainer) {
                    currentReviewId = idContainer.getAttribute(REVIEW_ID_ATTRIBUTE);
                }
            }

            if (currentReviewId === reviewId) {
                return reviewBlock;
            }
        }
        return null;
    }

    /**
     * 清除所有正在进行的高亮效果和计时器。
     */
    function clearHighlight() {
        if (highlightTimeoutId) {
            clearTimeout(highlightTimeoutId);
            highlightTimeoutId = null;
        }
        if (currentHighlightedElement) {
            currentHighlightedElement.classList.remove(HIGHLIGHT_CLASS);
            currentHighlightedElement = null;
        }
        console.log('[Coupang Review Finder] 已清除之前的高亮。');
    }

    /**
     * 滚动到指定评论元素并给予短暂的高亮效果。
     * @param {HTMLElement} element 要高亮的评论元素。
     * @param {string} reviewId 评论ID,用于日志记录。
     */
    function highlightAndScrollToReview(element, reviewId) {
        clearHighlight(); // 在高亮新元素前清除所有旧的

        console.log(`[Coupang Review Finder] 滚动并高亮评论: ${reviewId}`);
        element.scrollIntoView({ behavior: 'smooth', block: 'center' });

        // 给一个短暂的延迟,确保滚动完成后再添加高亮
        setTimeout(() => {
            if (element && document.body.contains(element)) { // 再次检查元素是否存在于DOM中
                element.classList.add(HIGHLIGHT_CLASS);
                currentHighlightedElement = element; // 标记当前高亮的元素

                // 设置一个计时器,在高亮2秒后自动移除
                highlightTimeoutId = setTimeout(() => {
                    if (currentHighlightedElement) {
                        currentHighlightedElement.classList.remove(HIGHLIGHT_CLASS);
                        currentHighlightedElement = null;
                        console.log(`[Coupang Review Finder] 评论 ${reviewId} 高亮已移除。`);
                    }
                }, 2000); // 2秒后移除高亮
            } else {
                console.warn(`[Coupang Review Finder] 尝试高亮评论 ${reviewId} 失败:元素不再存在于DOM。`);
            }
        }, 500); // 0.5秒延迟
    }

    /**
     * 创建并添加一个浮动按钮到页面,用于触发评论查找和高亮功能。
     */
    function createFindAndHighlightButton() {
        const button = document.createElement('button');
        button.className = 'tm-userscript-button';
        button.innerText = '查找并高亮评论';
        button.style.top = '10px';
        button.style.left = '10px';
        document.body.appendChild(button);

        button.addEventListener('click', () => {
            hideReviewIdListPanel(); // 隐藏ID列表面板

            const reviewId = prompt('请输入要查找的评论 reviewId (例如: 756185841):');
            if (!reviewId) {
                console.log('[Coupang Review Finder] 用户未输入 reviewId。操作取消。');
                return;
            }
            console.log(`[Coupang Review Finder] 开始查找 reviewId: ${reviewId}`);
            const targetReviewElement = findReviewElementById(reviewId);

            if (targetReviewElement) {
                highlightAndScrollToReview(targetReviewElement, reviewId);

                // 临时改变按钮文本,告知用户
                button.innerText = `找到并高亮评论 ${reviewId}!`;
                setTimeout(() => {
                    button.innerText = '查找并高亮评论'; // 恢复按钮文本
                }, 3000); // 3秒后恢复
            } else {
                console.warn(`[Coupang Review Finder] 未找到 reviewId 为 ${reviewId} 的评论。`);
                button.innerText = `未找到评论 ${reviewId}!`;
                setTimeout(() => {
                    button.innerText = '查找并高亮评论'; // 恢复按钮文本
                }, 3000); // 3秒后恢复
            }
        });
    }

    let reviewIdListPanel = null; // 存储评论ID列表面板元素

    /**
     * 创建并获取评论ID列表面板元素。
     * @returns {HTMLElement} 评论ID列表面板。
     */
    function getOrCreateReviewIdListPanel() {
        if (reviewIdListPanel) return reviewIdListPanel;

        reviewIdListPanel = document.createElement('div');
        reviewIdListPanel.id = REVIEW_ID_LIST_PANEL_ID;
        document.body.appendChild(reviewIdListPanel);

        // 点击面板外部关闭面板
        document.addEventListener('click', (event) => {
            const listButton = document.querySelector('[data-role="show-review-ids-button"]');
            if (reviewIdListPanel && reviewIdListPanel.style.display !== 'none' &&
                !reviewIdListPanel.contains(event.target) && !listButton.contains(event.target)) {
                hideReviewIdListPanel();
            }
        });
        return reviewIdListPanel;
    }

    /**
     * 生成并显示评论ID到面板中。
     * @param {Array<string>} reviewIds 评论ID数组。
     */
    function displayReviewIdsInPanel(reviewIds) {
        const panel = getOrCreateReviewIdListPanel();
        panel.innerHTML = ''; // 清空旧内容

        if (reviewIds.length === 0) {
            panel.innerHTML = '<p style="margin:0; text-align: center;">未找到评论ID。</p>';
        } else {
            reviewIds.forEach(id => {
                const idItem = document.createElement('a'); // 使用a标签更像链接
                idItem.href = '#'; // 无实际跳转
                idItem.className = 'tm-review-id-item';
                idItem.textContent = id;
                idItem.title = `点击跳转到评论 ${id}`;
                idItem.addEventListener('click', (event) => {
                    event.preventDefault(); // 阻止默认的链接跳转行为
                    const targetElement = findReviewElementById(id);
                    if (targetElement) {
                        highlightAndScrollToReview(targetElement, id);
                        hideReviewIdListPanel(); // 点击后隐藏面板
                    } else {
                        console.warn(`[Coupang Review Finder] 评论ID ${id} 已失效或未加载。`);
                        const originalText = idItem.textContent;
                        idItem.textContent = `${originalText} (未找到)`;
                        idItem.style.backgroundColor = '#ffc0cb'; // 红色背景提示
                        setTimeout(() => {
                            idItem.textContent = originalText;
                            idItem.style.backgroundColor = '';
                        }, 2000);
                    }
                });
                panel.appendChild(idItem);
            });
        }
        panel.style.display = 'block'; // 显示面板
    }

    /**
     * 隐藏评论ID列表面板。
     */
    function hideReviewIdListPanel() {
        if (reviewIdListPanel) {
            reviewIdListPanel.style.display = 'none';
        }
    }

    /**
     * 创建并添加一个浮动按钮到页面,用于显示所有评论ID。
     */
    function createListReviewIdsButton() {
        const button = document.createElement('button');
        button.className = 'tm-userscript-button';
        button.innerText = '显示所有评论ID';
        button.style.top = '60px'; // 放置在第一个按钮下方
        button.style.left = '10px';
        button.setAttribute('data-role', 'show-review-ids-button'); // 用于判断点击是否在按钮上
        document.body.appendChild(button);

        button.addEventListener('click', (event) => {
            event.stopPropagation(); // 阻止事件冒泡到document的点击监听器

            if (reviewIdListPanel && reviewIdListPanel.style.display === 'block') {
                hideReviewIdListPanel(); // 如果已经显示,则隐藏
                // 恢复按钮文本
                button.innerText = '显示所有评论ID';
            } else {
                clearHighlight(); // 显示面板时清除高亮

                const reviewBlocks = document.querySelectorAll(SINGLE_REVIEW_BLOCK_SELECTOR);
                const reviewIds = [];

                reviewBlocks.forEach(reviewBlock => {
                    let currentReviewId = reviewBlock.getAttribute(REVIEW_ID_ATTRIBUTE);
                    if (!currentReviewId) {
                        const idContainer = reviewBlock.querySelector(REVIEW_ID_CONTAINER_SELECTOR);
                        if (idContainer) {
                            currentReviewId = idContainer.getAttribute(REVIEW_ID_ATTRIBUTE);
                        }
                    }
                    if (currentReviewId) {
                        reviewIds.push(currentReviewId);
                    }
                });

                displayReviewIdsInPanel(reviewIds);
                if (reviewIds.length > 0) {
                    console.log(`%c[Coupang Review Finder] 当前页面找到 ${reviewIds.length} 个评论ID (已显示在面板中):`, 'color: green; font-weight: bold;');
                    console.log(reviewIds.join(', '));
                    button.innerText = `已找到 ${reviewIds.length} 个ID`;
                } else {
                    console.log('%c[Coupang Review Finder] 当前页面未找到任何评论ID。', 'color: orange; font-weight: bold;');
                    button.innerText = '未找到评论ID!';
                }
            }
        });
    }

    /**
     * 创建并添加一个浮动按钮到页面,用于查找Top评论。
     */
    function createFindTopReviewButton() {
        const button = document.createElement('button');
        button.className = 'tm-userscript-button';
        button.innerText = '查找Top评论';
        button.style.top = '110px'; // 放置在第三个位置
        button.style.left = '10px';
        document.body.appendChild(button);

        button.addEventListener('click', () => {
            // 每次点击时都重新查找,以应对动态加载的评论
            topReviewElements = Array.from(document.querySelectorAll(SINGLE_REVIEW_BLOCK_SELECTOR))
                .filter(el => el.querySelector(TOP_REVIEW_BADGE_SELECTOR));

            if (topReviewElements.length === 0) {
                button.innerText = '未找到Top评论';
                setTimeout(() => { button.innerText = '查找Top评论'; }, 2000);
                return;
            }

            currentTopReviewIndex++;
            if (currentTopReviewIndex >= topReviewElements.length) {
                currentTopReviewIndex = 0; // 从头开始循环
            }

            const targetReview = topReviewElements[currentTopReviewIndex];
            const reviewId = targetReview.querySelector(REVIEW_ID_CONTAINER_SELECTOR)?.getAttribute(REVIEW_ID_ATTRIBUTE) || `Top评论 #${currentTopReviewIndex + 1}`;

            highlightAndScrollToReview(targetReview, reviewId);

            button.innerText = `高亮Top评论 (${currentTopReviewIndex + 1}/${topReviewElements.length})`;
        });
    }

    // A small initial delay to allow the page to render basic structure
    window.addEventListener('load', () => {
        // 使用 MutationObserver 来监听 DOM 变化,更可靠地等待评论区出现
        const targetNode = document.body;
        const config = { childList: true, subtree: true };

        const createButtonsOnce = () => {
             // 检查是否已经创建过按钮
            if (document.querySelector('.tm-userscript-button')) {
                return;
            }
            console.log('[Coupang Review Finder] 评论区域检测到,创建按钮。');
            createFindAndHighlightButton();
            createListReviewIdsButton();
            createFindTopReviewButton();
            // 立即创建面板容器,但默认隐藏
            getOrCreateReviewIdListPanel();
        }

        const callback = function(mutationsList, observer) {
            const existingReviewBlock = document.querySelector(SINGLE_REVIEW_BLOCK_SELECTOR);
            if (existingReviewBlock) {
                observer.disconnect(); // 找到后停止观察,避免重复创建
                createButtonsOnce();
            }
        };

        // 仅在脚本加载后等待一小段时间,然后再启动观察,以避免在页面初次加载时过早触发
        setTimeout(() => {
            const initialReviewBlocks = document.querySelector(SINGLE_REVIEW_BLOCK_SELECTOR);
            if (initialReviewBlocks) {
                console.log('[Coupang Review Finder] 评论区域已在初始加载时存在,直接创建按钮。');
                createButtonsOnce();
            } else {
                console.log('[Coupang Review Finder] 评论区域尚未加载,启动 MutationObserver 监听。');
                const observer = new MutationObserver(callback);
                observer.observe(targetNode, config);
                // 设置一个备用超时,以防 MutationObserver 错过了(不太可能,但有个保险)
                setTimeout(() => {
                    if (!document.querySelector('.tm-userscript-button')) { // 检查按钮是否已创建
                        console.warn('[Coupang Review Finder] MutationObserver 超时或未检测到评论,尝试强制创建按钮。');
                        createButtonsOnce();
                        observer.disconnect(); // 停止观察
                    }
                }, 10000); // 10秒后强制创建
            }
        }, 1000); // 页面加载后1秒再启动检查
    });

})();