// ==UserScript==
// @name 亚马逊评论计算优化版(Enhanced Amazon Review Calculator)
// @namespace https://github.com/monty8800/amazon-seller-tools
// @version 3.4
// @description 精确计算各星级评价数量及提升评分所需五星好评数,支持全球亚马逊站点
// @author Monty & Assistant
// @match *://*.amazon.com/*dp/*
// @match *://*.amazon.co.uk/*dp/*
// @match *://*.amazon.de/*dp/*
// @match *://*.amazon.fr/*dp/*
// @match *://*.amazon.it/*dp/*
// @match *://*.amazon.es/*dp/*
// @match *://*.amazon.co.jp/*dp/*
// @match *://*.amazon.ca/*dp/*
// @match *://*.amazon.com.au/*dp/*
// @match *://*.amazon.in/*dp/*
// @match *://*.amazon.com.mx/*dp/*
// @match *://*.amazon.com.br/*dp/*
// @match *://*.amazon.nl/*dp/*
// @match *://*.amazon.cn/*dp/*
// @match *://*.amazon.sg/*dp/*
// @match *://*.amazon.ae/*dp/*
// @match *://*.amazon.sa/*dp/*
// @match *://*.amzn.*/*dp/*
// @icon https://www.amazon.com/favicon.ico
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
GM_addStyle(`
.monty-review-box {
border: 1px solid #ddd;
padding: 12px;
margin: 10px 0;
background: #f8f8f8;
border-radius: 4px;
}
.monty-review-title {
font-weight: bold;
color: #111;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
#monty-target-score {
font-size: 12px;
padding: 2px 4px;
border: 1px solid #ddd;
border-radius: 3px;
width: 50px;
}
.monty-review-item {
margin: 4px 0;
font-size: 13px;
}
.monty-highlight {
color: #B12704;
font-weight: bold;
}
.monty-lang-selector {
font-size: 12px;
padding: 2px 5px;
border: 1px solid #ddd;
border-radius: 3px;
background: white;
cursor: pointer;
}
.monty-lang-selector:hover {
border-color: #aaa;
}
`);
(function() {
'use strict';
const DEFAULT_TARGET_SCORE = 4.3;
const DEBUG_MODE = true; // 生产环境中关闭调试模式
// 获取用户设置的目标分数
function getTargetScore() {
const savedScore = GM_getValue('target_score', DEFAULT_TARGET_SCORE);
// 确保分数在1-5之间
return Math.min(5, Math.max(1, parseFloat(savedScore)));
}
// 设置用户目标分数
function setTargetScore(score) {
const validScore = Math.min(5, Math.max(1, parseFloat(score)));
log('设置目标分数:', validScore);
GM_setValue('target_score', validScore);
return validScore;
}
// 日志输出函数
function log(...args) {
if (DEBUG_MODE) {
console.log('[Review Calculator]', ...args);
}
}
// 获取用户语言偏好
function getUserLanguage() {
// 获取用户保存的语言偏好,默认为中文
const savedLanguage = GM_getValue('user_language', 'zh');
log('用户语言偏好:', savedLanguage);
return savedLanguage;
}
// 设置用户语言偏好
function setUserLanguage(language) {
log('设置用户语言偏好:', language);
GM_setValue('user_language', language);
}
// 获取本地化文本
function getLocalizedText() {
// 优先使用用户选择的语言
const userLanguage = getUserLanguage();
// 如果没有用户选择的语言,则根据域名自动检测
if (!userLanguage || userLanguage === 'auto') {
const domain = window.location.hostname;
log('当前域名:', domain);
// 根据域名确定语言
let detectedLanguage = 'en'; // 默认英语
if (domain.includes('.fr')) detectedLanguage = 'fr';
else if (domain.includes('.de')) detectedLanguage = 'de';
else if (domain.includes('.it')) detectedLanguage = 'it';
else if (domain.includes('.es')) detectedLanguage = 'es';
else if (domain.includes('.co.jp') || domain.includes('.jp')) detectedLanguage = 'jp';
else if (domain.includes('.cn')) detectedLanguage = 'zh';
else if (domain.includes('.nl')) detectedLanguage = 'nl';
else if (domain.includes('.com.br')) detectedLanguage = 'pt-br';
else if (domain.includes('.com.mx')) detectedLanguage = 'es-mx';
else if (domain.includes('.in')) detectedLanguage = 'en-in';
else if (domain.includes('.ca')) detectedLanguage = domain.includes('/fr/') ? 'fr-ca' : 'en-ca';
log('检测到语言:', detectedLanguage);
return getLocalizedTextByLanguage(detectedLanguage);
}
return getLocalizedTextByLanguage(userLanguage);
}
// 根据指定语言获取本地化文本
function getLocalizedTextByLanguage(language) {
log('使用语言:', language);
// 各种语言的本地化文本
const localizedTexts = {
// 评论数文本
ratingsText: {
'en': 'ratings',
'fr': 'évaluations',
'de': 'Bewertungen',
'it': 'recensioni',
'es': 'valoraciones',
'es-mx': 'calificaciones',
'jp': '件の評価',
'zh': '条评论',
'nl': 'beoordelingen',
'pt-br': 'avaliações',
'en-in': 'ratings',
'en-ca': 'ratings',
'fr-ca': 'évaluations'
},
// 星级文本 (用于匹配评分文本)
starText: {
'en': 'out of 5 stars',
'fr': 'sur 5 étoiles',
'de': 'von 5 Sternen',
'it': 'su 5 stelle',
'es': 'de 5 estrellas',
'es-mx': 'de 5 estrellas',
'jp': '5つ星のうち',
'zh': '5 星,最多 5 星',
'nl': 'van de 5 sterren',
'pt-br': 'de 5 estrelas',
'en-in': 'out of 5 stars',
'en-ca': 'out of 5 stars',
'fr-ca': 'sur 5 étoiles'
},
// 结果面板文本
resultText: {
'en': {
title: 'Review Analysis',
currentScore: 'Current Rating:',
required: 'Need additional',
fiveStarReviews: '5-star reviews',
toReach: 'to reach',
noNeed: 'Current rating already exceeds',
noNeedSuffix: ', no additional reviews needed',
error: 'Review Analysis Error',
errorHelp: 'Please refresh the page or contact the developer for help',
targetScore: 'Target Score'
},
'fr': {
title: 'Analyse des avis',
currentScore: 'Note actuelle:',
required: 'Besoin de',
fiveStarReviews: 'avis 5 étoiles supplémentaires',
toReach: 'pour atteindre',
noNeed: 'La note actuelle dépasse déjà',
noNeedSuffix: ', aucun avis supplémentaire nécessaire',
error: 'Erreur d\'analyse',
errorHelp: 'Veuillez rafraîchir la page ou contacter le développeur pour obtenir de l\'aide',
targetScore: 'Score cible'
},
'de': {
title: 'Bewertungsanalyse',
currentScore: 'Aktuelle Bewertung:',
required: 'Benötigt zusätzlich',
fiveStarReviews: '5-Sterne-Bewertungen',
toReach: 'um zu erreichen',
noNeed: 'Aktuelle Bewertung überschreitet bereits',
noNeedSuffix: ', keine zusätzlichen Bewertungen erforderlich',
error: 'Analysefehler',
errorHelp: 'Bitte aktualisieren Sie die Seite oder kontaktieren Sie den Entwickler für Hilfe',
targetScore: 'Zielbewertung'
},
'zh': {
title: '评论分析结果',
currentScore: '当前评分:',
required: '需要额外',
fiveStarReviews: '个五星好评',
toReach: '才能达到',
noNeed: '当前评分已达到',
noNeedSuffix: '分,无需额外好评',
error: '评论分析出错',
errorHelp: '请刷新页面或联系开发者获取帮助',
targetScore: '目标分数'
},
'jp': {
title: 'レビュー分析',
currentScore: '現在の評価:',
required: 'あと',
fiveStarReviews: '件の5つ星レビューが必要',
toReach: '目標の',
noNeed: '現在の評価は既に',
noNeedSuffix: 'を超えています。追加レビュー不要',
error: '分析エラー',
errorHelp: 'ページを更新するか、開発者にお問い合わせください',
targetScore: '目標スコア'
}
// 可以根据需要添加更多语言
}
};
// 如果没有特定语言的翻译,使用英语作为后备
const getTextWithFallback = (category, lang) => {
return localizedTexts[category][lang] || localizedTexts[category]['en'];
};
return {
ratingsText: getTextWithFallback('ratingsText', language),
starText: getTextWithFallback('starText', language),
resultText: localizedTexts.resultText[language] || localizedTexts.resultText['en']
};
}
// 清洗数字格式(处理千位分隔符)
function sanitizeNumber(numStr) {
return numStr.replace(/[.,\s]/g, '')
.replace(/[^\d]/g, '');
}
// 计算加权平均分
function calculateWeightedAverage(ratings) {
const total = ratings.reduce((sum, r) => sum + r.count, 0);
if (total === 0) return 0;
return ratings.reduce((sum, r) => {
return sum + (r.stars * r.count);
}, 0) / total;
}
// 计算所需五星好评
function calculateRequiredReviews(currentScore, totalReviews, targetScore = getTargetScore()) {
// 如果当前评分已经达到目标,则不需要额外的好评
if (currentScore >= targetScore) {
return 0;
}
// 特殊情况:如果目标分数是5分,使用不同的计算方法
if (targetScore >= 5) {
// 计算达到5分需要的五星评价数
// 公式:(5 * (totalReviews + x) - currentScore * totalReviews) / (totalReviews + x) = 5
// 简化:x = (5 - currentScore) * totalReviews / (5 - 5) 无法计算
// 因此使用另一种方法:计算将所有非5星评价抵消所需的5星评价数
const nonFiveStarWeight = totalReviews * (5 - currentScore);
return Math.ceil(nonFiveStarWeight);
}
// 常规情况:计算公式
// (目标评分 * (总评论数 + x) - 当前评分 * 总评论数) / (总评论数 + x) = 目标评分
// 简化后:x = (目标评分 * 总评论数 - 当前评分 * 总评论数) / (5 - 目标评分)
const numerator = targetScore * totalReviews - currentScore * totalReviews;
const denominator = 5 - targetScore;
return Math.ceil(numerator / denominator);
}
// 主处理函数
async function processReviews() {
try {
log('开始处理评论数据...');
log('当前URL:', window.location.href);
// 等待评分直方图加载 - 使用最新的选择器
log('等待评分直方图加载...');
const histogram = await waitForElement('#histogramTable');
if (!histogram) {
log('错误: 找不到评分直方图');
throw new Error('找不到评分直方图');
}
log('成功找到评分直方图:', histogram);
// 获取本地化文本
const localizedText = getLocalizedText();
log('本地化文本:', localizedText);
// 直接使用data-hook属性查找总评论数
const totalElement = document.querySelector('[data-hook="total-review-count"]');
log('总评论数元素:', totalElement);
if (!totalElement) {
log('错误: 找不到总评论数元素');
throw new Error('找不到总评论数元素');
}
log('总评论数文本:', totalElement.textContent);
const totalReviews = parseInt(sanitizeNumber(totalElement.textContent));
log('解析后的总评论数:', totalReviews);
if (isNaN(totalReviews)) {
log('错误: 总评论数格式错误');
throw new Error('总评论数格式错误');
}
// 获取各星级评价 - 使用最新的选择器
log('查找评分条...');
const ratingBars = [...document.querySelectorAll('#histogramTable li a')];
log('找到评分条数量:', ratingBars.length);
if (ratingBars.length !== 5) {
log('错误: 找不到完整的五星评价数据, 只找到', ratingBars.length, '条');
throw new Error('找不到完整的五星评价数据');
}
log('开始提取各星级评价数据...');
const ratings = ratingBars.map((bar, index) => {
// 获取星级 (5星到1星)
const stars = 5 - index;
log(`处理 ${stars} 星评价...`);
// 获取百分比 - 从aria-valuenow属性获取
let percent = 0;
const meter = bar.querySelector('.a-meter');
log(`${stars}星评价条元素:`, meter);
if (meter && meter.getAttribute('aria-valuenow')) {
percent = parseInt(meter.getAttribute('aria-valuenow')) / 100;
log(`${stars}星评价 - 从aria-valuenow获取百分比:`, percent);
}
// 如果无法从aria-valuenow获取,尝试从style.width获取
if (percent === 0 && meter && meter.querySelector('.a-meter-bar')) {
const meterBar = meter.querySelector('.a-meter-bar');
const widthStyle = meterBar.style.width;
log(`${stars}星评价 - meter-bar宽度样式:`, widthStyle);
if (widthStyle) {
percent = parseInt(widthStyle) / 100;
log(`${stars}星评价 - 从style.width获取百分比:`, percent);
}
}
// 如果仍然无法获取百分比,尝试从文本中提取
if (percent === 0) {
log(`${stars}星评价 - 尝试从文本提取百分比...`);
const percentTexts = bar.querySelectorAll('.a-text-right, .aok-nowrap');
log(`${stars}星评价 - 找到可能包含百分比的文本元素:`, percentTexts.length);
for (const el of percentTexts) {
log(`${stars}星评价 - 文本内容:`, el.textContent);
const percentMatch = el.textContent.match(/(\d+)%/);
if (percentMatch) {
percent = parseInt(percentMatch[1]) / 100;
log(`${stars}星评价 - 从文本提取的百分比:`, percent);
break;
}
}
}
const count = Math.round(totalReviews * percent);
log(`${stars}星评价 - 最终数据:`, { stars, percent, count });
return {
stars: stars,
percent: percent,
count: count
};
});
// 计算当前评分
log('计算加权平均分...');
const currentScore = calculateWeightedAverage(ratings);
log('计算得到的当前评分:', currentScore);
// 获取目标分数
const targetScore = getTargetScore();
log('目标评分:', targetScore);
// 计算结果
log('计算所需五星好评数...');
const required = calculateRequiredReviews(currentScore, totalReviews, targetScore);
log('需要的五星好评数:', required);
// 生成结果面板
const resultBox = document.createElement('div');
resultBox.className = 'monty-review-box';
resultBox.id = 'monty-review-box';
// 使用本地化文本
const rt = localizedText.resultText;
// 创建语言选择器
const currentLang = getUserLanguage();
const langOptions = {
'zh': '中文',
'en': 'English',
'fr': 'Français',
'de': 'Deutsch',
'jp': '日本語'
};
const langSelector = `
<select class="monty-lang-selector" id="monty-lang-selector">
${Object.entries(langOptions).map(([code, name]) =>
`<option value="${code}" ${code === currentLang ? 'selected' : ''}>${name}</option>`
).join('')}
</select>
`;
resultBox.innerHTML = `
<div class="monty-review-title">
<span>${rt.title}</span>
<div style="display: flex; align-items: center; gap: 8px;">
<div style="display: flex; align-items: center; font-size: 12px;">
<label for="monty-target-score" style="margin-right: 4px;">${rt.targetScore || '目标分数'}:</label>
<input type="number" id="monty-target-score" min="1" max="5" step="0.1" value="${targetScore}"
style="width: 50px; padding: 2px 4px; border: 1px solid #ddd; border-radius: 3px;">
</div>
${langSelector}
</div>
</div>
${ratings.map(r => `
<div class="monty-review-item">
${'★'.repeat(r.stars)} ${r.count} (${(r.percent*100).toFixed(1)}%)
</div>
`).join('')}
<hr style="margin:8px 0">
<div class="monty-review-item">
${rt.currentScore} <span class="monty-highlight">${currentScore.toFixed(2)}</span>
</div>
${required > 0 ? `
<div class="monty-review-item">
${rt.required} <span class="monty-highlight">${required} ${rt.fiveStarReviews}</span>
${rt.toReach} ${targetScore}
</div>
` : `
<div class="monty-review-item monty-highlight">
${currentScore >= targetScore ?
`${rt.noNeed} ${targetScore}${rt.noNeedSuffix}` :
`${targetScore === 5 ? '无法达到满分5.0,需要无限个五星好评' : ''}`}
</div>
`}
<div class="monty-review-item" style="font-size: 12px; margin-top: 10px; text-align: right; color: #555; border-top: 1px solid #eee; padding-top: 8px;">
2025 Monty Ng. All rights reserved.
</div>
`;
// 保存评分数据,以便在切换语言时重新生成结果面板
resultBox.dataset.currentScore = currentScore;
resultBox.dataset.totalReviews = totalReviews;
resultBox.dataset.required = required;
resultBox.dataset.ratingsData = JSON.stringify(ratings);
// 插入结果到页面 - 使用更准确的插入点
log('准备插入结果面板到页面...');
const possibleInsertPoints = [
'#averageCustomerReviews',
'#histogramTable',
'[data-hook="cr-filter-info-review-rating-count"]',
'.cr-widget-histogram'
];
log('尝试以下插入点:', possibleInsertPoints);
let inserted = false;
for (const selector of possibleInsertPoints) {
const insertPoint = document.querySelector(selector);
log(`检查插入点 ${selector}:`, insertPoint ? '找到' : '未找到');
if (insertPoint) {
// 尝试插入到元素之后
if (insertPoint.parentNode) {
log(`将结果面板插入到 ${selector} 之后`);
insertPoint.parentNode.insertBefore(resultBox, insertPoint.nextSibling);
inserted = true;
break;
}
}
}
// 如果无法找到合适的插入点,则插入到直方图之前
if (!inserted) {
log('未找到理想插入点,尝试使用直方图作为插入点');
if (histogram && histogram.parentNode) {
log('将结果面板插入到直方图之前');
histogram.parentNode.insertBefore(resultBox, histogram);
inserted = true;
} else {
log('警告: 无法找到任何插入点');
}
}
log('结果面板插入' + (inserted ? '成功' : '失败'));
// 添加语言选择器的事件监听器
if (inserted) {
const langSelector = document.getElementById('monty-lang-selector');
if (langSelector) {
langSelector.addEventListener('change', function() {
const newLang = this.value;
log('切换语言到:', newLang);
setUserLanguage(newLang);
// 重新生成结果面板
regenerateResultPanel(resultBox);
});
}
}
// 添加目标分数输入框事件监听器
const targetScoreInput = document.getElementById('monty-target-score');
if (targetScoreInput) {
targetScoreInput.addEventListener('change', function() {
const newScore = parseFloat(this.value);
if (!isNaN(newScore) && newScore >= 1 && newScore <= 5) {
log('修改目标分数:', newScore);
const validScore = setTargetScore(newScore);
this.value = validScore; // 确保显示有效的值
// 重新计算所需评论数
const currentScore = parseFloat(resultBox.dataset.currentScore);
const totalReviews = parseInt(resultBox.dataset.totalReviews);
const newRequired = calculateRequiredReviews(currentScore, totalReviews, validScore);
resultBox.dataset.required = newRequired;
// 重新生成结果面板
regenerateResultPanel(resultBox);
} else {
// 恢复为有效值
this.value = getTargetScore();
}
});
}
// 重新生成结果面板的函数
function regenerateResultPanel(panel) {
if (!panel) return;
// 获取保存的数据
const currentScore = parseFloat(panel.dataset.currentScore);
const totalReviews = parseInt(panel.dataset.totalReviews);
let required = parseInt(panel.dataset.required);
const ratings = JSON.parse(panel.dataset.ratingsData);
// 获取当前目标分数
const targetScore = getTargetScore();
// 获取新的本地化文本
const localizedText = getLocalizedText();
const rt = localizedText.resultText;
// 创建语言选择器
const currentLang = getUserLanguage();
const langOptions = {
'zh': '中文',
'en': 'English',
'fr': 'Français',
'de': 'Deutsch',
'jp': '日本語'
};
const langSelector = `
<select class="monty-lang-selector" id="monty-lang-selector">
${Object.entries(langOptions).map(([code, name]) =>
`<option value="${code}" ${code === currentLang ? 'selected' : ''}>${name}</option>`
).join('')}
</select>
`;
// 更新面板内容
panel.innerHTML = `
<div class="monty-review-title">
<span>${rt.title}</span>
<div style="display: flex; align-items: center; gap: 8px;">
<div style="display: flex; align-items: center; font-size: 12px;">
<label for="monty-target-score" style="margin-right: 4px;">${rt.targetScore || '目标分数'}:</label>
<input type="number" id="monty-target-score" min="1" max="5" step="0.1" value="${targetScore}"
style="width: 50px; padding: 2px 4px; border: 1px solid #ddd; border-radius: 3px;">
</div>
${langSelector}
</div>
</div>
${ratings.map(r => `
<div class="monty-review-item">
${'★'.repeat(r.stars)} ${r.count} (${(r.percent*100).toFixed(1)}%)
</div>
`).join('')}
<hr style="margin:8px 0">
<div class="monty-review-item">
${rt.currentScore} <span class="monty-highlight">${currentScore.toFixed(2)}</span>
</div>
${required > 0 ? `
<div class="monty-review-item">
${rt.required} <span class="monty-highlight">${required} ${rt.fiveStarReviews}</span>
${rt.toReach} ${targetScore}
</div>
` : `
<div class="monty-review-item monty-highlight">
${currentScore >= targetScore ?
`${rt.noNeed} ${targetScore}${rt.noNeedSuffix}` :
`${targetScore === 5 ? '无法达到满分5.0,需要无限个五星好评' : ''}`}
</div>
`}
<div class="monty-review-item" style="font-size: 12px; margin-top: 10px; text-align: right; color: #555; border-top: 1px solid #eee; padding-top: 8px;">
2025 Monty Ng. All rights reserved.
</div>
`;
// 重新添加事件监听器
const newLangSelector = document.getElementById('monty-lang-selector');
if (newLangSelector) {
newLangSelector.addEventListener('change', function() {
const newLang = this.value;
log('切换语言到:', newLang);
setUserLanguage(newLang);
// 重新生成结果面板
regenerateResultPanel(panel);
});
}
// 添加目标分数输入框事件监听器
const newTargetScoreInput = document.getElementById('monty-target-score');
if (newTargetScoreInput) {
newTargetScoreInput.addEventListener('change', function() {
const newScore = parseFloat(this.value);
if (!isNaN(newScore) && newScore >= 1 && newScore <= 5) {
log('修改目标分数:', newScore);
const validScore = setTargetScore(newScore);
this.value = validScore; // 确保显示有效的值
// 重新计算所需评论数
const currentScore = parseFloat(panel.dataset.currentScore);
const totalReviews = parseInt(panel.dataset.totalReviews);
const newRequired = calculateRequiredReviews(currentScore, totalReviews, validScore);
panel.dataset.required = newRequired;
// 重新生成结果面板
regenerateResultPanel(panel);
} else {
// 恢复为有效值
this.value = getTargetScore();
}
});
}
}
} catch (error) {
if (DEBUG_MODE) console.error('[Review Calculator]', error);
// 在页面上显示错误信息,帮助用户理解问题
showError(`计算评论数据时出错: ${error.message}`);
// 尝试使用备用方法获取评分
log('主方法失败,尝试使用备用方法...');
try {
// 尝试从页面上直接获取平均评分
log('尝试从页面直接获取平均评分...');
const ratingElement = document.querySelector('[data-hook="average-star-rating"] .a-icon-alt');
log('评分元素:', ratingElement);
if (ratingElement) {
log('评分文本:', ratingElement.textContent);
const ratingMatch = ratingElement.textContent.match(/(\d+(\.\d+)?)/);
log('评分匹配结果:', ratingMatch);
if (ratingMatch) {
const currentScore = parseFloat(ratingMatch[1]);
log('解析后的评分:', currentScore);
// 尝试获取总评论数
log('尝试获取总评论数...');
const totalElement = document.querySelector('[data-hook="total-review-count"]');
log('总评论数元素:', totalElement);
if (totalElement) {
log('总评论数文本:', totalElement.textContent);
const totalReviews = parseInt(sanitizeNumber(totalElement.textContent));
log('解析后的总评论数:', totalReviews);
if (!isNaN(totalReviews) && !isNaN(currentScore)) {
log('成功获取评分和总评论数,计算所需五星好评...');
// 计算所需五星好评
const required = calculateRequiredReviews(currentScore, totalReviews);
log('需要的五星好评数:', required);
// 获取本地化文本
const localizedText = getLocalizedText();
const rt = localizedText.resultText;
// 创建简化版结果面板
log('创建简化版结果面板...');
const simpleBox = document.createElement('div');
simpleBox.className = 'monty-review-box';
simpleBox.id = 'monty-simple-review-box';
// 创建语言选择器
const currentLang = getUserLanguage();
const langOptions = {
'zh': '中文',
'en': 'English',
'fr': 'Français',
'de': 'Deutsch',
'jp': '日本語'
};
const langSelector = `
<select class="monty-lang-selector" id="monty-simple-lang-selector">
${Object.entries(langOptions).map(([code, name]) =>
`<option value="${code}" ${code === currentLang ? 'selected' : ''}>${name}</option>`
).join('')}
</select>
`;
simpleBox.innerHTML = `
<div class="monty-review-title">
<span>${rt.title} ${rt.simplified}</span>
${langSelector}
</div>
<div class="monty-review-item">
${rt.currentScore} <span class="monty-highlight">${currentScore.toFixed(2)}</span>
</div>
${required > 0 ? `
<div class="monty-review-item">
${rt.required} <span class="monty-highlight">${required} ${rt.fiveStarReviews}</span>
${rt.toReach} ${TARGET_SCORE}
</div>
` : `
<div class="monty-review-item monty-highlight">
${rt.noNeed} ${TARGET_SCORE}${rt.noNeedSuffix}
</div>
`}
<div class="monty-review-item">
<small>${rt.note}</small>
</div>
<div class="monty-review-item" style="font-size: 12px; margin-top: 10px; text-align: right; color: #555; border-top: 1px solid #eee; padding-top: 8px;">
© 2025 Monty Ng. All rights reserved.
</div>
`;
// 保存评分数据,以便在切换语言时重新生成结果面板
simpleBox.dataset.currentScore = currentScore;
simpleBox.dataset.totalReviews = totalReviews;
simpleBox.dataset.required = required;
// 尝试插入到页面
log('尝试插入简化版结果面板...');
const insertPoint = document.querySelector('#averageCustomerReviews');
log('插入点:', insertPoint);
if (insertPoint && insertPoint.parentNode) {
log('将简化版结果面板插入到页面');
insertPoint.parentNode.insertBefore(simpleBox, insertPoint.nextSibling);
log('简化版结果面板插入成功');
// 添加语言选择器的事件监听器
const simpleLangSelector = document.getElementById('monty-simple-lang-selector');
if (simpleLangSelector) {
simpleLangSelector.addEventListener('change', function() {
const newLang = this.value;
log('切换语言到:', newLang);
setUserLanguage(newLang);
// 重新生成简化版结果面板
regenerateSimpleResultPanel(simpleBox);
});
}
} else {
log('警告: 无法找到插入点');
}
// 重新生成简化版结果面板的函数
function regenerateSimpleResultPanel(panel) {
if (!panel) return;
// 获取保存的数据
const currentScore = parseFloat(panel.dataset.currentScore);
const totalReviews = parseInt(panel.dataset.totalReviews);
const required = parseInt(panel.dataset.required);
// 获取新的本地化文本
const localizedText = getLocalizedText();
const rt = localizedText.resultText;
// 创建语言选择器
const currentLang = getUserLanguage();
const langOptions = {
'zh': '中文',
'en': 'English',
'fr': 'Français',
'de': 'Deutsch',
'jp': '日本語'
};
const langSelector = `
<select class="monty-lang-selector" id="monty-simple-lang-selector">
${Object.entries(langOptions).map(([code, name]) =>
`<option value="${code}" ${code === currentLang ? 'selected' : ''}>${name}</option>`
).join('')}
</select>
`;
// 更新面板内容
panel.innerHTML = `
<div class="monty-review-title">
<span>${rt.title} ${rt.simplified}</span>
${langSelector}
</div>
<div class="monty-review-item">
${rt.currentScore} <span class="monty-highlight">${currentScore.toFixed(2)}</span>
</div>
${required > 0 ? `
<div class="monty-review-item">
${rt.required} <span class="monty-highlight">${required} ${rt.fiveStarReviews}</span>
${rt.toReach} ${TARGET_SCORE}
</div>
` : `
<div class="monty-review-item monty-highlight">
${rt.noNeed} ${TARGET_SCORE}${rt.noNeedSuffix}
</div>
`}
<div class="monty-review-item">
<small>${rt.note}</small>
</div>
<div class="monty-review-item" style="font-size: 12px; margin-top: 10px; text-align: right; color: #555; border-top: 1px solid #eee; padding-top: 8px;">
© 2025 Monty Ng. All rights reserved.
</div>
`;
// 重新添加事件监听器
const newLangSelector = document.getElementById('monty-simple-lang-selector');
if (newLangSelector) {
newLangSelector.addEventListener('change', function() {
const newLang = this.value;
log('切换语言到:', newLang);
setUserLanguage(newLang);
// 重新生成简化版结果面板
regenerateSimpleResultPanel(panel);
});
}
}
} else {
log('错误: 无效的评分或总评论数');
}
} else {
log('错误: 找不到总评论数元素');
}
} else {
log('错误: 无法从文本中提取评分');
}
} else {
log('错误: 找不到评分元素');
}
} catch (backupError) {
log('备用方法也失败:', backupError);
if (DEBUG_MODE) console.error('[Review Calculator] 备用方法也失败:', backupError);
}
}
}
// 辅助函数:等待元素加载
function waitForElement(selector, timeout = 5000) {
return new Promise((resolve) => {
// 如果元素已存在,立即返回
const existingEl = document.querySelector(selector);
if (existingEl) return resolve(existingEl);
// 否则,设置观察器等待元素出现
const start = Date.now();
// 创建一个MutationObserver来监视DOM变化
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
observer.disconnect(); // 停止观察
return resolve(el);
}
// 超时检查
if (Date.now() - start > timeout) {
observer.disconnect();
return resolve(null);
}
});
// 开始观察DOM变化
observer.observe(document.body, {
childList: true,
subtree: true
});
// 额外的超时保障
setTimeout(() => {
observer.disconnect();
resolve(document.querySelector(selector));
}, timeout);
});
}
// 显示错误信息
function showError(message) {
if (DEBUG_MODE) console.error('[Review Calculator]', message);
// 获取本地化文本
const localizedText = getLocalizedText();
const rt = localizedText.resultText;
// 在页面上显示错误信息
const errorBox = document.createElement('div');
errorBox.className = 'monty-review-box';
errorBox.innerHTML = `
<div class="monty-review-title">${rt.error}</div>
<div class="monty-review-item">${message}</div>
<div class="monty-review-item">
<small>${rt.errorHelp}</small>
</div>
<div class="monty-review-item" style="font-size: 12px; margin-top: 10px; text-align: right; color: #555; border-top: 1px solid #eee; padding-top: 8px;">
© 2025 Monty Ng. All rights reserved.
</div>
`;
// 尝试插入到评论区域
const insertPoints = [
'#cm_cr-review_list',
'.cr-widget-histogram',
'#histogramTable',
'#averageCustomerReviews',
'#reviewsMedley'
];
for (const selector of insertPoints) {
const element = document.querySelector(selector);
if (element) {
element.parentNode.insertBefore(errorBox, element);
return;
}
}
// 如果找不到合适的插入点,插入到页面底部
document.body.appendChild(errorBox);
}
// 初始化
function init() {
log('初始化脚本...');
const targetScore = getTargetScore();
log('目标评分:', targetScore);
// 确保我们在产品页面上
log('当前页面路径:', window.location.pathname);
if (!window.location.pathname.includes('/dp/')) {
log('不是产品页面,脚本不执行');
return;
}
// 等待DOM完全加载
log('当前文档状态:', document.readyState);
if (document.readyState === 'loading') {
log('文档仍在加载中,等待DOMContentLoaded事件...');
document.addEventListener('DOMContentLoaded', () => {
log('DOM已加载,延迟1500ms执行主函数');
setTimeout(processReviews, 1500);
});
} else {
// 如果DOM已加载,给页面一些时间来完成动态内容加载
log('DOM已加载,延迟1500ms执行主函数');
setTimeout(processReviews, 1500);
}
}
// 启动脚本
try {
log('脚本开始执行...');
log('浏览器信息:', navigator.userAgent);
init();
} catch (error) {
log('初始化失败:', error);
showError(`初始化失败: ${error.message}`);
}
})();