Booth 价格换算(JPY -> CNY)

在 Booth 平台上显示日元换算后的人民币价格,0 显示"免费"并添加下划线

// ==UserScript==
// @name         Booth 价格换算(JPY -> CNY)
// @namespace    http://tampermonkey.net/
// @homepage     https://github.com/vikiboss/booth-price-cny
// @version      3.2
// @description  在 Booth 平台上显示日元换算后的人民币价格,0 显示"免费"并添加下划线
// @author       Viki <[email protected]> (https://github.com/vikiboss)
// @match        https://booth.pm/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      60s.viki.moe
// @connect      60s-api.viki.moe
// ==/UserScript==

(function() {
    'use strict';

    // 配置
    const CACHE_KEY = 'jpy_to_cny_rate';
    const CACHE_DATE_KEY = 'jpy_to_cny_date';
    const DEFAULT_RATE = 0.05; // 默认汇率 1:20
    const PROCESSED_MARK = 'data-jpy-processed';
    const ZERO_PRICE_MARK = 'data-zero-price';

    // 全局变量
    let exchangeRate = DEFAULT_RATE;
    let debugMode = false; // 调试模式,设为false减少日志输出

    // 日志输出
    function log(message, data) {
        if (debugMode) {
            console.log(`[JPY2CNY] ${message}`, data || '');
        }
    }

    // 获取今天的日期字符串 YYYY-MM-DD
    function getTodayString() {
        const today = new Date();
        return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
    }

    // 解析价格
    function parsePrice(price) {
        return parseFloat(price.replace(/[,\s]/g, ''));
    }

    // 格式化人民币金额
    function formatCNY(jpy, isZero) {
        if (isZero) {
            return '免费';
        }
        return `${Math.round(jpy * exchangeRate*100) / 100} CNY`;
    }

    // 添加CSS样式
    function addGlobalStyle() {
        const styleElement = document.createElement('style');
        styleElement.textContent = `
            [${ZERO_PRICE_MARK}="true"] {
                text-decoration: underline;
                text-decoration-style: solid;
                text-decoration-color: #ff9900;
                text-decoration-thickness: 2px;
            }
        `;
        document.head.appendChild(styleElement);
    }

    // 获取汇率(带缓存)
    async function getExchangeRate() {
        const cachedRate = GM_getValue(CACHE_KEY);
        const cachedDate = GM_getValue(CACHE_DATE_KEY);
        const todayString = getTodayString();

        // 如果缓存有效且是今天的数据,直接使用缓存
        if (cachedRate && cachedDate === todayString) {
            log(`使用缓存汇率: ${cachedRate}`);
            return cachedRate;
        }

        // 尝试获取新汇率
        return new Promise((resolve) => {
            log('开始请求汇率API...');

            GM_xmlhttpRequest({
                method: 'GET',
                url: 'https://60s-api.viki.moe/v2/exchange_rate?currency=jpy',
                timeout: 5000,
                onload: function(response) {
                    try {
                        const data = JSON.parse(response.responseText);
                        const cnyRate = data?.data?.rates?.find(r => r.currency === 'CNY')?.rate;

                        if (cnyRate) {
                            log(`获取到新汇率: ${cnyRate}`);
                            GM_setValue(CACHE_KEY, cnyRate);
                            GM_setValue(CACHE_DATE_KEY, todayString);
                            resolve(cnyRate);
                        } else {
                            resolve(cachedRate || DEFAULT_RATE);
                        }
                    } catch (error) {
                        resolve(cachedRate || DEFAULT_RATE);
                    }
                },
                onerror: function() {
                    resolve(cachedRate || DEFAULT_RATE);
                }
            });
        });
    }

    // 处理文本节点中的价格
    function processTextNode(node) {
 if (node.nodeType !== Node.TEXT_NODE) return false;

    const text = node.nodeValue;
    if (!text || text.trim() === '') return false;

    // 防止重复处理,检查是否已包含CNY或免费字样
    if (text.includes('CNY') || text.includes('免费')) return false;

    // 修复后的正则表达式和处理逻辑
    const patterns = [
        // 匹配 "数字 JPY" 格式
        {
            regex: /(\d[\d,]+|\d+)\s*JPY/gi,
            process: function(text) {
                return text.replace(/(\d[\d,]+|\d+)\s*JPY/gi, function(match) {
                    // 提取纯数字部分
                    const priceDigits = match.replace(/[^\d]/g, '');
                    const jpyValue = parseFloat(priceDigits);
                    const isZero = jpyValue === 0;

                    // 标记零价格,供后续添加样式
                    node._hasZeroPrice = isZero;
                    node._price = jpyValue;

                    return `${match} (${formatCNY(jpyValue, isZero)})`;
                });
            }
        },
        // 匹配 "¥数字" 格式 - 修复了正则表达式
        {
            regex: /¥\s*(\d[\d,]*)/g, // 改为与替换正则一致
            process: function(text) {
                return text.replace(/¥\s*(\d[\d,]*)/g, function(match, digits) {
                    // 提取纯数字部分
                    const priceDigits = digits.replace(/[,\s]/g, '');
                    const jpyValue = parseFloat(priceDigits);
                    const isZero = jpyValue === 0;

                    // 标记零价格,供后续添加样式
                    node._hasZeroPrice = isZero;
                    node._price = jpyValue;

                    return `${match}(${formatCNY(jpyValue, isZero)})`;
                });
            }
        }
    ];

        let newText = text;
        let modified = false;

        // 依次尝试每个匹配模式
        for (const pattern of patterns) {
            if (pattern.regex.test(text)) {
                // 使用分离的处理函数改变文本
                const processed = pattern.process(text);
                newText = processed;
                modified = true;
                break;
            }
        }

        // 如果文本被修改,更新节点并设置样式
        if (modified) {
            node.nodeValue = newText;

            // 找到合适的父元素进行缩放和样式设置
            let parent = findAppropriateParent(node);

            if (parent) {
                // 应用变换和样式
                applyStyles(parent, node._hasZeroPrice);

                // 存储价格信息,用于后续检查
                parent._priceInfo = {
                    isZero: node._hasZeroPrice,
                    price: node._price
                };

                return true;
            }
        }

        return false;
    }

    // 查找适合应用样式的父元素
    function findAppropriateParent(node) {
        if (!node || !node.parentElement) return null;

        // 从当前节点开始向上查找
        let parent = node.parentElement;

        // 跳过简单的内联元素
        while (parent && ['SPAN', 'STRONG', 'B', 'I', 'EM'].includes(parent.tagName) &&
              parent.childNodes.length === 1) {
            parent = parent.parentElement;
        }

        // 检查是否已有之前处理过的父元素
        let current = parent;
        while (current) {
            if (current.hasAttribute(PROCESSED_MARK)) {
                // 更新已处理元素的零价格状态
                updateZeroPriceStatus(current, node._hasZeroPrice);
                return current;
            }
            current = current.parentElement;
        }

        return parent;
    }

    // 更新元素的零价格状态
    function updateZeroPriceStatus(element, isZero) {
        if (isZero) {
            element.setAttribute(ZERO_PRICE_MARK, 'true');
        } else {
            element.removeAttribute(ZERO_PRICE_MARK);
        }
    }

    // 应用样式到元素
    function applyStyles(element, isZero) {
        if (!element.hasAttribute(PROCESSED_MARK)) {
            element.setAttribute(PROCESSED_MARK, 'true');
            element.style.transform = 'scale(0.8)';
            element.style.transformOrigin = 'left center';

            // 确保正确显示
            if (getComputedStyle(element).display === 'inline') {
                element.style.display = 'inline-block';
            }
        }

        // 设置零价格标记
        updateZeroPriceStatus(element, isZero);
    }

    // 处理DOM元素
    function processElement(element) {
        if (!element || element.nodeType !== Node.ELEMENT_NODE) return;

        // 跳过不需要处理的元素
        if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'IFRAME'].includes(element.tagName)) {
            return;
        }

        // 使用TreeWalker查找所有文本节点
        const walker = document.createTreeWalker(
            element,
            NodeFilter.SHOW_TEXT,
            {
                acceptNode: function(node) {
                    // 过滤掉空文本节点
                    return node.nodeValue.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
                }
            },
            false
        );

        const nodesToProcess = [];
        let textNode;
        while (textNode = walker.nextNode()) {
            nodesToProcess.push(textNode);
        }

        // 处理收集到的文本节点
        nodesToProcess.forEach(processTextNode);
    }

    // 设置DOM变化监听器
    function observeDOM() {
        const observer = new MutationObserver((mutations) => {
            mutations.forEach(mutation => {
                // 处理新添加的节点
                if (mutation.type === 'childList' && mutation.addedNodes.length) {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            processElement(node);
                        } else if (node.nodeType === Node.TEXT_NODE && node.parentNode) {
                            processTextNode(node);
                        }
                    });
                }
                // 处理文本变化
                else if (mutation.type === 'characterData') {
                    // 如果文本节点变化,重新处理
                    processTextNode(mutation.target);

                    // 找到处理过这个节点的父元素,检查并更新状态
                    let parent = mutation.target.parentElement;
                    while (parent) {
                        if (parent._priceInfo) {
                            const oldState = parent._priceInfo.isZero;
                            const newState = mutation.target._hasZeroPrice;

                            // 如果零价格状态改变,更新样式
                            if (oldState !== newState) {
                                updateZeroPriceStatus(parent, newState);
                                parent._priceInfo.isZero = newState;
                            }
                            break;
                        }
                        parent = parent.parentElement;
                    }
                }
            });
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true,
            characterData: true
        });
    }

    // 初始化函数
    async function init() {
        try {
            // 添加全局样式
            addGlobalStyle();

            // 获取汇率
            exchangeRate = await getExchangeRate();
            log(`使用汇率: ${exchangeRate}`);

            // 处理当前页面
            processElement(document.body);

            // 设置监听器
            observeDOM();
        } catch (error) {
            log(`初始化错误: ${error.message}`, error);
        }
    }

    // 启动脚本
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();