Greasy Fork

来自缓存

Greasy Fork is available in English.

亚马逊搜索结果添加序号和广告ID(自用)

为亚马逊搜索结果页面上的广告和自然搜索结果添加序号,并为所有广告(含品牌广告)添加广告ID和活动ID,全部显示在商品卡片上。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         亚马逊搜索结果添加序号和广告ID(自用)
// @version      2.3.1
// @description  为亚马逊搜索结果页面上的广告和自然搜索结果添加序号,并为所有广告(含品牌广告)添加广告ID和活动ID,全部显示在商品卡片上。
// @author       Haer
// @match        https://www.amazon.com/*
// @match        https://www.amazon.co.uk/*
// @match        https://www.amazon.de/*
// @match        https://www.amazon.it/*
// @match        https://www.amazon.fr/*
// @match        https://www.amazon.es/*
// @match        https://www.amazon.se/*
// @match        https://www.amazon.com.mx/*
// @match        https://www.amazon.co.jp/*
// @match        https://www.amazon.ca/*
// @icon         https://www.amazon.com/favicon.ico
// @license      MIT
// @grant        GM_setClipboard
// @run-at       document-end
// @namespace http://greasyfork.icu/users/1124651
// ==/UserScript==

(function () {
    'use strict';

    /************ 多语言 ************/
    const translations = {
        'zh': { AdID: '广告ID', CampaignID: '活动ID', Copy: '复制', Copied: '已复制' },
        'en': { AdID: 'Ad ID', CampaignID: 'Campaign ID', Copy: 'Copy', Copied: 'Copied' },
        'de': { AdID: 'Werbe-ID', CampaignID: 'Kampagnen-ID', Copy: 'Kopieren', Copied: 'Kopiert' },
        'fr': { AdID: 'ID Publicité', CampaignID: 'ID Campagne', Copy: 'Copier', Copied: 'Copié' },
        'es': { AdID: 'ID Anuncio', CampaignID: 'ID Campaña', Copy: 'Copiar', Copied: 'Copiado' },
        'it': { AdID: 'ID Annuncio', CampaignID: 'ID Campagna', Copy: 'Copia', Copied: 'Copiato' },
        'nl': { AdID: 'Advertentie-ID', CampaignID: 'Campagne-ID', Copy: 'Kopiëren', Copied: 'Gekopieerd' },
        'ja': { AdID: '広告ID', CampaignID: 'キャンペーンID', Copy: 'コピー', Copied: 'コピー済み' }
    };

    function getLanguage() {
        const hostname = window.location.hostname;
        const countryMap = {
            'amazon.co.jp': 'ja',
            'amazon.de': 'de',
            'amazon.fr': 'fr',
            'amazon.es': 'es',
            'amazon.it': 'it',
            'amazon.co.uk': 'en',
            'amazon.ca': 'en',
            'amazon.com': 'en',
            'amazon.com.mx': 'es',
            'amazon.se': 'en'
        };
        for (const [domain, lang] of Object.entries(countryMap)) {
            if (hostname.includes(domain)) return lang;
        }
        const lang = (navigator.language || navigator.userLanguage || 'en').split('-')[0];
        return translations[lang] ? lang : 'en';
    }

    const lang = getLanguage();
    const t = translations[lang];

    /************ 工具函数 ************/
    function safeJSONParse(str) {
        try {
            return JSON.parse(str);
        } catch (e) {
            return null;
        }
    }

    function getUrlParam(url, name) {
        try {
            const u = new URL(url, location.origin);
            return u.searchParams.get(name);
        } catch (e) {
            return null;
        }
    }

    /************ 复制标签 ************/
    function createCopyableLabel(text, title, backgroundColor) {
        const container = document.createElement('span');
        container.style.display = 'inline-flex';
        container.style.alignItems = 'center';
        container.style.gap = '4px';
        container.style.marginRight = '8px';
        container.style.flexWrap = 'nowrap';

        const label = document.createElement('span');
        label.textContent = text;
        label.title = title || text;
        label.style.backgroundColor = backgroundColor;
        label.style.color = '#fff';
        label.style.padding = '2px 8px';
        label.style.borderRadius = '4px';
        label.style.fontSize = '12px';
        label.style.userSelect = 'text';
        label.style.webkitUserSelect = 'text';
        label.style.whiteSpace = 'nowrap';
        label.style.maxWidth = '180px';
        label.style.overflow = 'hidden';
        label.style.textOverflow = 'ellipsis';

        const copyBtn = document.createElement('span');
        copyBtn.textContent = '📋';
        copyBtn.title = t.Copy;
        copyBtn.style.cursor = 'pointer';
        copyBtn.style.fontSize = '11px';
        copyBtn.style.opacity = '0.8';
        copyBtn.style.flexShrink = '0';

        const copyText = text.split(': ').pop() || text;

        copyBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            e.preventDefault();

            if (typeof GM_setClipboard !== 'undefined') {
                GM_setClipboard(copyText, 'text');
            } else {
                const ta = document.createElement('textarea');
                ta.value = copyText;
                ta.style.position = 'fixed';
                ta.style.opacity = '0';
                document.body.appendChild(ta);
                ta.select();
                try {
                    document.execCommand('copy');
                } catch (err) {}
                document.body.removeChild(ta);
            }

            const old = copyBtn.textContent;
            copyBtn.textContent = '✓';
            copyBtn.style.color = '#00c853';
            setTimeout(() => {
                copyBtn.textContent = old;
                copyBtn.style.color = '';
            }, 1200);
        });

        container.appendChild(label);
        container.appendChild(copyBtn);
        return container;
    }

    function createNumberLabel(text, isAd) {
        const label = document.createElement('span');
        label.textContent = text;
        label.style.backgroundColor = isAd ? '#FF9900' : '#146EB4';
        label.style.color = '#fff';
        label.style.padding = '2px 8px';
        label.style.borderRadius = '4px';
        label.style.fontSize = '12px';
        label.style.fontWeight = 'bold';
        label.style.minWidth = isAd ? '45px' : '32px';
        label.style.textAlign = 'center';
        label.style.marginRight = '8px';
        label.style.flexShrink = '0';
        return label;
    }

    /************ 收集 asin → 广告元数据(SP + multi-ad + 带 asin 的品牌广告) ************/
    function collectAsinAdMap() {
        const asinToAd = new Map();

        // 1)SP 广告:data-s-safe-ajax-modal-trigger
        document.querySelectorAll('[data-s-safe-ajax-modal-trigger]').forEach(el => {
            const raw = el.getAttribute('data-s-safe-ajax-modal-trigger');
            if (!raw) return;
            const cfg = safeJSONParse(raw);
            if (!cfg || !cfg.ajaxUrl) return;

            const pl = getUrlParam(cfg.ajaxUrl, 'pl');
            if (!pl) return;

            const decoded = decodeURIComponent(pl);
            const payload = safeJSONParse(decoded);
            if (!payload || !payload.adCreativeMetaData) return;

            const details = payload.adCreativeMetaData.adCreativeDetails || [];
            details.forEach(d => {
                if (d.asin && d.adId && d.campaignId && !asinToAd.has(d.asin)) {
                    asinToAd.set(d.asin, {
                        adId: d.adId,
                        campaignId: d.campaignId
                    });
                }
            });
        });

        // 2)multi-ad:data-multi-ad-feedback-form-trigger
        document.querySelectorAll('[data-multi-ad-feedback-form-trigger]').forEach(el => {
            const raw = el.getAttribute('data-multi-ad-feedback-form-trigger');
            if (!raw) return;
            const cfg = safeJSONParse(raw);
            if (!cfg || !cfg.multiAdfPayload) return;

            const payload = safeJSONParse(cfg.multiAdfPayload);
            if (!payload || !payload.adCreativeMetaData) return;

            const details = payload.adCreativeMetaData.adCreativeDetails || [];
            details.forEach(d => {
                if (d.asin && d.adId && d.campaignId && !asinToAd.has(d.asin)) {
                    asinToAd.set(d.asin, {
                        adId: d.adId,
                        campaignId: d.campaignId
                    });
                }
            });
        });

        // 3)带 asin 的 data-ad-feedback(品牌广告):creativeDetails 里如果有 asin 也并入
        document.querySelectorAll('[data-ad-feedback]').forEach(el => {
            const raw = el.getAttribute('data-ad-feedback');
            if (!raw) return;
            const payload = safeJSONParse(raw);
            if (!payload) return;

            const details = payload.creativeDetails || payload.creativeDetail || [];
            details.forEach(d => {
                const adId = d.adId || d.adID;
                const campaignId = d.campaignId || d.campaignID;
                const asin = d.asin;
                if (!asin || !adId || !campaignId) return;
                if (!asinToAd.has(asin)) {
                    asinToAd.set(asin, {
                        adId,
                        campaignId
                    });
                }
            });
        });

        return asinToAd;
    }

    /************ 判断是否为“显式 Sponsored” ************/
    function isAdSearchResultByLabel(result) {
        if (!result) return false;
        if (result.classList.contains('AdHolder')) return true;
        if (result.getAttribute('data-component-type') === 'sp-sponsored-result') return true;
        if (result.querySelector('.puis-sponsored-label-text, .s-sponsored-label-text, .s-widget-sponsored-label-text')) return true;
        return false;
    }

    /************ 在商品标题下插入标签 ************/
    function insertLabelUnderTitle(result, labelContainer) {
        const title =
            result.querySelector('h2, h2 a, [data-cy="title-recipe"]') ||
            result.querySelector('a.a-link-normal.s-underline-text');

        if (title && title.parentElement) {
            title.parentElement.insertBefore(labelContainer, title.nextSibling);
        } else {
            result.insertBefore(labelContainer, result.firstChild);
        }
    }

    /************ 主逻辑:给搜索结果加序号和广告ID ************/
    function applyLabelsToResults() {
        // 清理旧的
        document.querySelectorAll('.amazon-adhelper-label').forEach(el => el.remove());

        const asinToAd = collectAsinAdMap();
        const results = document.querySelectorAll('[data-component-type="s-search-result"][data-asin]');
        let adIndex = 0;
        let naturalIndex = 0;
        const seenAsinNatural = new Set();

        results.forEach(result => {
            const asin = result.getAttribute('data-asin');
            if (!asin) return;

            const meta = asinToAd.get(asin);  // 是否在广告反馈里出现过
            const hasSponsoredLabel = isAdSearchResultByLabel(result);

            // 关键修正:只要这个 ASIN 被识别为广告(出现在广告反馈里),就当广告处理
            const isAd = hasSponsoredLabel || !!meta;

            const box = document.createElement('div');
            box.className = 'amazon-adhelper-label';
            box.style.display = 'flex';
            box.style.alignItems = 'center';
            box.style.flexWrap = 'wrap';
            box.style.gap = '6px';
            box.style.margin = '4px 0';

            if (isAd) {
                adIndex++;
                box.appendChild(createNumberLabel('AD' + adIndex, true));

                if (meta && meta.adId) {
                    box.appendChild(
                        createCopyableLabel(`${t.AdID}: ${meta.adId}`, `${t.AdID}: ${meta.adId}`, '#232F3E')
                    );
                }
                if (meta && meta.campaignId) {
                    box.appendChild(
                        createCopyableLabel(`${t.CampaignID}: ${meta.campaignId}`, `${t.CampaignID}: ${meta.campaignId}`, '#37475A')
                    );
                }
            } else {
                // 自然位:同一个 ASIN 只记一次自然序号
                if (seenAsinNatural.has(asin)) return;
                seenAsinNatural.add(asin);
                naturalIndex++;
                box.appendChild(createNumberLabel('#' + naturalIndex, false));
            }

            insertLabelUnderTitle(result, box);
        });
    }

    /************ 监听 DOM & URL 变化 ************/
    let applyTimer = null;
    function scheduleApply() {
        clearTimeout(applyTimer);
        applyTimer = setTimeout(applyLabelsToResults, 800);
    }

    window.addEventListener('load', scheduleApply);
    document.addEventListener('DOMContentLoaded', scheduleApply);

    const domObserver = new MutationObserver(() => {
        if (document.querySelector('[data-component-type="s-search-result"]')) {
            scheduleApply();
        }
    });
    domObserver.observe(document.documentElement || document.body, {
        childList: true,
        subtree: true
    });

    let lastUrl = location.href;
    const urlObserver = new MutationObserver(() => {
        const current = location.href;
        if (current !== lastUrl) {
            lastUrl = current;
            scheduleApply();
        }
    });
    urlObserver.observe(document.body, { childList: true, subtree: true });

})();