Greasy Fork

Greasy Fork is available in English.

EPIC游戏库存导出

修复日期提取问题,精确提取EPIC游戏订单信息,基于HTML表格结构。

当前为 2025-06-29 提交的版本,查看 最新版本

// ==UserScript==
// @name         EPIC游戏库存导出
// @namespace    http://tampermonkey.net/
// @version      1.5
// @license PaperTiger
// @description  修复日期提取问题,精确提取EPIC游戏订单信息,基于HTML表格结构。
// @author       Paper Tiger
// @match         *://*.epicgames.com/account/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.0/xlsx.full.min.js
// @grant        none
// ==/UserScript==

/* global XLSX */

(function() {
    'use strict';

    let isProcessing = false;
    let currentPage = 0;
    let allOrderData = [];
    let processedOrderIds = new Set(); // 用于跟踪已处理的订单ID

    function createExportButton() {
        const button = document.createElement('button');
        button.textContent = '开始导出';
        button.id = 'epic-export-btn';
        button.style.position = 'fixed';
        button.style.top = '10px';
        button.style.right = '400px';
        button.style.zIndex = '99999';
        button.style.padding = '10px';
        button.style.backgroundColor = 'rgba(40, 167, 69, 1)';
        button.style.color = 'white';
        button.style.border = '2px solid red';
        button.style.borderRadius = '5px';
        button.style.cursor = 'pointer';
        document.body.appendChild(button);

        // 创建进度显示区域
        const progressDiv = document.createElement('div');
        progressDiv.id = 'epic-progress';
        progressDiv.style.position = 'fixed';
        progressDiv.style.top = '50px';
        progressDiv.style.right = '400px';
        progressDiv.style.zIndex = '99999';
        progressDiv.style.padding = '10px';
        progressDiv.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
        progressDiv.style.color = 'white';
        progressDiv.style.borderRadius = '5px';
        progressDiv.style.display = 'none';
        progressDiv.style.maxWidth = '300px';
        document.body.appendChild(progressDiv);

        button.addEventListener('click', function() {
            if (!isProcessing) {
                isProcessing = true;
                button.textContent = '处理中...';
                button.disabled = true;
                allOrderData = []; // 清空之前的数据
                processedOrderIds.clear(); // 清空已处理的订单ID
                currentPage = 0;
                clickTransactions();
            }
        });
    }

    function updateProgress(message) {
        const progressDiv = document.getElementById('epic-progress');
        if (progressDiv) {
            progressDiv.style.display = 'block';
            progressDiv.innerHTML = message;
        }
    }

    function clickTransactions() {
        const transactionsButton = document.querySelector('#nav-link-transactions');
        if (transactionsButton) {
            transactionsButton.click();
            console.log('点击了交易按钮');
            updateProgress('正在加载交易页面...');
            // 等待页面加载后开始提取数据
            setTimeout(extractCurrentPageData, 2000);
        } else {
            console.log("找不到交易按钮。");
            updateProgress('找不到交易按钮,请确保在正确的页面上。');
            resetButton();
        }
    }

    function extractCurrentPageData() {
        currentPage++;
        updateProgress(`正在处理第 ${currentPage} 页...`);

        // 查找所有订单行 - 使用更精确的选择器
        const orderRows = document.querySelectorAll('tr[data-orderid]');
        console.log(`第 ${currentPage} 页找到 ${orderRows.length} 个订单`);

        let pageOrderCount = 0;
        orderRows.forEach((row, index) => {
            try {
                const orderData = extractOrderDataFromRow(row);
                if (orderData && orderData['订单ID']) {
                    // 检查订单ID是否已经处理过
                    if (!processedOrderIds.has(orderData['订单ID'])) {
                        processedOrderIds.add(orderData['订单ID']);
                        allOrderData.push(orderData);
                        pageOrderCount++;
                        console.log(`提取订单: ${orderData['订单ID']} - ${orderData['订单日期']} - ${orderData['游戏名称']} - ${orderData['付款金额']}`);
                    } else {
                        console.log(`跳过重复订单ID: ${orderData['订单ID']}`);
                    }
                } else {
                    console.warn(`第 ${index + 1} 个订单数据无效或缺少订单ID`);
                }
            } catch (error) {
                console.error(`提取第 ${index + 1} 个订单数据时出错:`, error);
            }
        });

        console.log(`第 ${currentPage} 页提取了 ${pageOrderCount} 个新订单,总计 ${allOrderData.length} 个订单`);

        // 检查是否有下一页
        setTimeout(checkNextPage, 1000);
    }

    function extractOrderDataFromRow(row) {
        try {
            // 获取订单ID - 确保从正确的属性获取
            const orderId = row.getAttribute('data-orderid');
            if (!orderId) {
                console.warn('订单行缺少data-orderid属性');
                return null;
            }

            // 获取订单信息单元格 (第二个td)
            const infoCell = row.querySelector('td:nth-child(2)');
            if (!infoCell) {
                console.warn('找不到订单信息单元格');
                return null;
            }

            // 提取日期 - 修复日期提取逻辑
            let orderDate = '';
            const mainDiv = infoCell.querySelector('.am-yd8sa2');
            if (mainDiv) {
                // 获取第一个文本节点,这应该是日期
                const firstChild = mainDiv.firstChild;
                if (firstChild && firstChild.nodeType === Node.TEXT_NODE) {
                    orderDate = firstChild.textContent.trim();
                } else {
                    // 如果没有直接的文本节点,尝试其他方法
                    const dateText = mainDiv.textContent;
                    // 使用正则表达式提取日期格式
                    const dateMatch = dateText.match(/(\d{4}年\d{1,2}月\d{1,2}日)/);
                    if (dateMatch) {
                        orderDate = dateMatch[1];
                    }
                }
            }

            // 提取游戏名称 - 查找包含游戏名称的span元素
            let gameName = '';
            const gameNameElements = infoCell.querySelectorAll('.am-hoct6b');
            if (gameNameElements.length > 0) {
                gameName = gameNameElements[0].textContent.trim();
            }

            // 提取价格信息 - 查找包含"价格"文本的元素
            let price = '0.00';
            const priceElements = infoCell.querySelectorAll('.am-brjg0');
            for (let element of priceElements) {
                if (element.textContent.includes('价格')) {
                    const priceContainer = element.nextElementSibling;
                    if (priceContainer && priceContainer.classList.contains('am-1v0j95h')) {
                        const priceText = priceContainer.textContent.trim();
                        // 提取价格数字,处理 "¥0.00" 或 "- ¥0.00" 格式
                        const priceMatch = priceText.match(/[¥$]?([\d.,]+)/);
                        if (priceMatch) {
                            price = priceMatch[1];
                        }
                        break;
                    }
                }
            }

            // 提取商城信息
            let marketplace = '';
            for (let element of priceElements) {
                if (element.textContent.includes('商城')) {
                    const marketplaceContainer = element.nextElementSibling;
                    if (marketplaceContainer && marketplaceContainer.classList.contains('am-1v0j95h')) {
                        marketplace = marketplaceContainer.textContent.trim();
                        break;
                    }
                }
            }

            // 提取说明信息
            let description = '';
            for (let element of priceElements) {
                if (element.textContent.includes('说明')) {
                    const descContainer = element.nextElementSibling;
                    if (descContainer && descContainer.classList.contains('am-1v0j95h')) {
                        const descElement = descContainer.querySelector('.am-1rqw9bo');
                        if (descElement) {
                            description = descElement.textContent.trim();
                        }
                        break;
                    }
                }
            }

            // 验证数据完整性
            if (!orderId || !orderDate) {
                console.warn(`订单数据不完整: ID=${orderId}, 日期=${orderDate}`);
                return null;
            }

            // 调试信息
            console.log(`调试 - 订单ID: ${orderId}, 日期: "${orderDate}", 游戏: "${gameName}"`);

            return {
                '订单ID': orderId,
                '订单日期': orderDate,
                '游戏名称': gameName,
                '说明': description,
                '付款金额': price,
                '商城': marketplace,
                '页面': currentPage
            };
        } catch (error) {
            console.error('提取订单数据时出错:', error);
            return null;
        }
    }

    function checkNextPage() {
        const nextButton = document.querySelector('#next-btn');
        if (nextButton && !nextButton.disabled) {
            nextButton.click();
            console.log('点击了下一页按钮');
            updateProgress(`正在加载第 ${currentPage + 1} 页...`);
            // 等待页面加载后继续提取
            setTimeout(extractCurrentPageData, 2000);
        } else {
            console.log("没有更多页面可以加载。");
            updateProgress(`处理完成!共提取 ${allOrderData.length} 个唯一订单。正在生成Excel文件...`);
            // 等待最后一页数据完全加载
            setTimeout(exportData, 2000);
        }
    }

    function exportData() {
        if (allOrderData.length === 0) {
            alert('没有提取到任何订单数据,请检查页面是否正确加载。');
            resetButton();
            return;
        }

        console.log(`准备导出 ${allOrderData.length} 条唯一订单数据`);

        const workbook = XLSX.utils.book_new();
        const worksheet = XLSX.utils.json_to_sheet(allOrderData);
        XLSX.utils.book_append_sheet(workbook, worksheet, '订单历史');
        XLSX.writeFile(workbook, `EPIC游戏订单历史_${new Date().toISOString().slice(0, 10)}.xlsx`);

        alert(`导出完成!共导出 ${allOrderData.length} 条唯一订单数据。`);
        resetButton();
    }

    function resetButton() {
        const button = document.getElementById('epic-export-btn');
        const progressDiv = document.getElementById('epic-progress');

        if (button) {
            button.textContent = '开始导出';
            button.disabled = false;
        }

        if (progressDiv) {
            progressDiv.style.display = 'none';
        }

        isProcessing = false;
    }

    createExportButton();
})();