淘寶人民幣轉新台幣(僅供參考)

Convert RMB to NTD on Taobao and Tmall with real-time exchange rate

// ==UserScript==
// @name         淘寶人民幣轉新台幣(僅供參考)
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Convert RMB to NTD on Taobao and Tmall with real-time exchange rate
// @author       Grok
// @match        *://*.taobao.com/*
// @match        *://*.tmall.com/*
// @exclude     https://buy.taobao.com/auction/order/confirm_order.htm
// @grant        GM_xmlhttpRequest
// @license     MIT
// ==/UserScript==

(async function() {
    'use strict';

    const DEFAULT_RATE = 4.5;
    let exchangeRate = DEFAULT_RATE;
    const selectors = {
        rightItem: {
            unit: '[class*="right-item-label"]',
            value: '.right-item-amount'
        },
        priceWrapper: {
            unit: '[class*="price-unit"]',
            value: '.price-value'
        },
        genericWrapper: {
            unit: '[class*="unit--"]',
            wrapper: '[class*="innerPriceWrapper--"]',
            value: '[class*="priceInt--"]'
        },
        highlightPrice: {
            container: '[class*="--highlightPrice--"]',
            unit: '[class*="--symbol--"]',
            value: '[class*="--text--"'
        },
        businessEntry: {
            unit: '.business-entry-item-card-content-coin-title',
            value: '.business-entry-item-card-content-coin-title + span'
        },
        tradePrice: {
            container: '.trade-price-container',
            unit: '.trade-price-symbol',
            integer: '.trade-price-integer',
            point: '.trade-price-point',
            decimal: '.trade-price-decimal'
        },
        priceWrap: {
            container: '[class*="priceWrap--"]',
            unit: '[class*="symbol--"]',
            wrapper: '[class*="price--"]'
        }
    };

    // Utility functions
    const log = (...args) => console.log('[RMB-NTD]', ...args);
    const fetchExchangeRate = () => new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
            method: 'GET',
            url: 'https://api.exchangerate-api.com/v4/latest/CNY',
            onload: res => resolve(JSON.parse(res.responseText).rates.TWD),
            onerror: reject
        });
    });

    const convertPrice = (rmb) => {
        return Math.round(rmb * exchangeRate * 100) / 100;
    };

    const processPriceElement = (unitEl, valueEl, context) => {
        if (!valueEl || (unitEl.textContent !== '¥' && unitEl.textContent !== '¥')) return false;

        const priceText = valueEl.textContent.trim();
        const priceMatch = priceText.match(/^(\d+(\.\d+)?)$/);
        if (!priceMatch) return false;

        const rmb = parseFloat(priceMatch[1]);
        const ntd = convertPrice(rmb);

        unitEl.textContent = '$';
        valueEl.textContent = ntd;
        valueEl.dataset.originalRmb = rmb;
        valueEl.dataset.convertedNtd = ntd;
        logcki(`Converted ¥${rmb} to $${ntd} in ${context}`);
        return true;
    };

    const processTradePriceElement = (container) => {
        const unit = container.querySelector(selectors.tradePrice.unit);
        const integer = container.querySelector(selectors.tradePrice.integer);
        const point = container.querySelector(selectors.tradePrice.point);
        const decimal = container.querySelector(selectors.tradePrice.decimal);

        if (!unit || (unit.textContent !== '¥' && unit.textContent !== '¥') || !integer) return false;

        let priceStr = integer.textContent;
        if (decimal && point) {
            priceStr += `${point.textContent}${decimal.textContent}`;
        }

        const rmb = parseFloat(priceStr);
        if (isNaN(rmb)) return false;

        const ntd = convertPrice(rmb);
        const [newInteger, newDecimal] = ntd.toString().split('.');

        unit.textContent = '$';
        integer.textContent = newInteger;
        integer.dataset.originalRmb = rmb;
        integer.dataset.convertedNtd = ntd;
        if (decimal && point) {
            point.textContent = '.';
            decimal.textContent = newDecimal || '00';
            decimal.dataset.originalRmb = rmb;
            decimal.dataset.convertedNtd = ntd;
        }

        log(`Converted ¥${rmb} to $${ntd} in trade-price`);
        return true;
    };

    const updateTradePriceElement = (container) => {
        const unit = container.querySelector(selectors.tradePrice.unit);
        const integer = container.querySelector(selectors.tradePrice.integer);
        const point = container.querySelector(selectors.tradePrice.point);
        const decimal = container.querySelector(selectors.tradePrice.decimal);

        if (!unit || !integer) return false;

        let currentPriceStr = integer.textContent;
        if (decimal && point) {
            currentPriceStr += `.${decimal.textContent}`;
        }

        const currentNum = parseFloat(currentPriceStr);
        if (isNaN(currentNum)) return false;

        if (integer.dataset.convertedNtd) {
            const expectedNtd = parseFloat(integer.dataset.convertedNtd);
            if (Math.abs(currentNum - expectedNtd) < 0.01) return false;
        }

        const rmb = currentNum;
        const ntd = convertPrice(rmb);
        const [newInteger, newDecimal] = ntd.toString().split('.');

        unit.textContent = '$';
        integer.textContent = newInteger;
        integer.dataset.originalRmb = rmb;
        integer.dataset.convertedNtd = ntd;
        if (decimal && point) {
            point.textContent = '.';
            decimal.textContent = newDecimal || '00';
            decimal.dataset.originalRmb = rmb;
            decimal.dataset.convertedNtd = ntd;
        }

        log(`Updated ¥${rmb} to $${ntd} in trade-price`);
        return true;
    };

    const setupTradePriceObserver = () => {
        const tradeContainers = document.querySelectorAll(selectors.tradePrice.container);
        tradeContainers.forEach(container => {
            const unit = container.querySelector(selectors.tradePrice.unit);
            const integer = container.querySelector(selectors.tradePrice.integer);
            const decimal = container.querySelector(selectors.tradePrice.decimal);

            const observerConfig = {
                childList: true,
                characterData: true,
                subtree: true
            };

            const handleChange = () => {
                if (!integer) return;

                if (unit.textContent === '¥' || unit.textContent === '¥') {
                    processTradePriceElement(container);
                } else {
                    updateTradePriceElement(container);
                }
            };

            if (integer) {
                new MutationObserver(handleChange).observe(integer, observerConfig);
            }
            if (decimal) {
                new MutationObserver(handleChange).observe(decimal, observerConfig);
            }
            if (unit) {
                new MutationObserver(handleChange).observe(unit, observerConfig);
            }
        });
    };

    const convertPrices = () => {
        let convertedCount = 0;

        document.querySelectorAll(selectors.rightItem.unit).forEach(unit => {
            if (processPriceElement(unit, unit.nextElementSibling, 'right-item')) {
                convertedCount++;
            }
        });

        document.querySelectorAll(selectors.priceWrapper.unit).forEach(unit => {
            if (processPriceElement(unit, unit.nextElementSibling, 'price-wrapper')) {
                convertedCount++;
            }
        });

        document.querySelectorAll(selectors.genericWrapper.unit).forEach(unit => {
            const wrapper = unit.nextElementSibling;
            if (wrapper?.matches(selectors.genericWrapper.wrapper)) {
                const value = wrapper.querySelector(selectors.genericWrapper.value);
                if (processPriceElement(unit, value, 'generic-wrapper')) {
                    convertedCount++;
                }
            }
        });

        document.querySelectorAll(selectors.highlightPrice.container).forEach(container => {
            const unit = container.querySelector(selectors.highlightPrice.unit);
            const value = container.querySelector(selectors.highlightPrice.value);
            if (processPriceElement(unit, value, 'highlight-price')) {
                convertedCount++;
            }
        });

        document.querySelectorAll(selectors.businessEntry.unit).forEach(unit => {
            const value = unit.nextElementSibling;
            if (processPriceElement(unit, value, 'business-entry')) {
                convertedCount++;
            }
        });

        document.querySelectorAll(selectors.tradePrice.container).forEach(container => {
            if (processTradePriceElement(container)) {
                convertedCount++;
            }
        });

        document.querySelectorAll(selectors.priceWrap.container).forEach(container => {
            const wrapper = container.querySelector(selectors.priceWrap.wrapper);
            const unit = wrapper?.querySelector(selectors.priceWrap.unit);
            if (unit && (unit.textContent === '¥' || unit.textContent === '¥')) {
                const priceTextNode = wrapper?.childNodes[1];
                if (priceTextNode?.nodeType === Node.TEXT_NODE) {
                    const priceText = priceTextNode.textContent.trim();
                    const priceMatch = priceText.match(/^(\d+(\.\d+)?)$/);
                    if (priceMatch) {
                        const rmb = parseFloat(priceMatch[1]);
                        const ntd = convertPrice(rmb);
                        
                        unit.textContent = '$';
                        priceTextNode.textContent = ntd;
                        wrapper.dataset.originalRmb = rmb;
                        wrapper.dataset.convertedNtd = ntd;
                        log(`Converted ¥${rmb} to $${ntd} in price-wrap`);
                        convertedCount++;
                    }
                }
            }
        });

        log(`Converted ${convertedCount} prices`);
        return convertedCount;
    };

    try {
        exchangeRate = await fetchExchangeRate();
        log('Exchange rate:', exchangeRate);
    } catch (error) {
        log('Using default rate:', exchangeRate, error);
    }

    convertPrices();
    setupTradePriceObserver();

    let timeoutId;
    const debouncedConvert = () => {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
            log('Updating prices...');
            convertPrices();
            setupTradePriceObserver();
        }, 500);
    };

    new MutationObserver(debouncedConvert).observe(document.body, {
        childList: true,
        subtree: true
    });

    setInterval(debouncedConvert, 2000);
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址