// ==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秒再启动检查
});
})();