LQFaKa 订单统计 (按钮弹窗版 v0.4 - Fetch+XHR)

通过按钮触发弹窗,显示拦截API响应(Fetch或XHR)统计的LQFaKa订单数据

// ==UserScript==
// @name         LQFaKa 订单统计 (按钮弹窗版 v0.4 - Fetch+XHR)
// @namespace    http://tampermonkey.net/
// @version      0.4
// @description  通过按钮触发弹窗,显示拦截API响应(Fetch或XHR)统计的LQFaKa订单数据
// @author       KING
// @match        https://new.lqfaka.com/order*
// @match        https://new.lqfaka.com/index/order*
// @grant        GM_addStyle
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    console.log("LQFaKa 订单统计脚本 v0.4 (Fetch+XHR) 开始运行");

    // --- 全局变量存储最新统计结果 ---
    let latestAmountStr = "N/A";
    let latestCount = 0;
    let latestApiTotal = "N/A";
    let dataReady = false;
    let lastErrorMessage = "";
    let statsButton = null; // Make button globally accessible within the IIFE
    let modalOverlay = null; // Make modal globally accessible

    // --- 更新按钮状态函数 ---
    function updateButtonStatus(ready, message = null) {
        if (statsButton) {
            if (ready) {
                statsButton.textContent = '查看统计';
                statsButton.disabled = false;
                statsButton.style.backgroundColor = '#28a745'; // Green for ready
                statsButton.style.color = 'white';
                 lastErrorMessage = ""; // Clear error on success
            } else {
                // 保留之前的错误信息,除非有新的 message
                if (message) lastErrorMessage = message;
                statsButton.textContent = message || '数据错误';
                statsButton.disabled = true;
                 statsButton.style.backgroundColor = '#dc3545'; // Red for error
                 statsButton.style.color = 'white';
            }
        }
    }

    // --- 处理从API获取的数据 ---
    function processOrderData(apiDataString) {
        console.log("尝试处理API数据:", apiDataString.substring(0, 200) + "..."); // Log first 200 chars
        try {
            const data = JSON.parse(apiDataString);
            console.log("成功解析订单 API JSON 数据:", data);

            if (data && data.code === 1 && data.data && Array.isArray(data.data.list)) {
                const orders = data.data.list;
                latestApiTotal = data.data.total;
                let calculatedAmount = 0;
                latestCount = orders.length;

                orders.forEach(order => {
                    const amount = parseFloat(order.total_amount);
                    if (!isNaN(amount)) {
                        calculatedAmount += amount;
                    }
                });
                latestAmountStr = calculatedAmount.toFixed(2);
                dataReady = true;
                console.log(`数据更新: 数量=${latestCount}, 金额=${latestAmountStr}, 总数=${latestApiTotal}`);
                updateButtonStatus(true); // Update button to ready state

            } else {
                console.warn("API 响应数据结构不符合预期或请求未成功:", data);
                lastErrorMessage = "无法从API响应中解析有效数据 (结构或code不符)。";
                // 不立即标记为错误,可能其他请求会成功
                // updateButtonStatus(false, "数据结构错误");
            }
        } catch (err) {
            console.error("解析订单 API JSON 数据时出错:", err);
            lastErrorMessage = "解析API响应JSON时出错: " + err.message;
             // 不立即标记为错误
            // updateButtonStatus(false, "数据解析错误");
        }
    }


    // --- 核心逻辑:拦截 fetch 请求 ---
    const originalFetch = window.fetch;
    window.fetch = function(url, options) {
        // Handle url being a string or a Request object
        const requestInfo = arguments[0];
        const requestUrl = (typeof requestInfo === 'string') ? requestInfo : requestInfo.url;
        console.log(`[Fetch Intercept] 发起请求: ${requestUrl}`); // Log ALL fetch requests

        const fetchPromise = originalFetch.apply(this, arguments);

        fetchPromise.then(response => {
            console.log(`[Fetch Intercept] 收到响应: ${response.url}, Status: ${response.status}`);
            // 检查是否是我们关心的 API 请求的响应
            if (response.url.includes('/shopApi/Order/list')) {
                console.log("[Fetch Intercept] 检测到目标订单 API 响应:", response.url);
                const clonedResponse = response.clone();
                clonedResponse.text().then(textData => { // Get text first for reliable parsing
                     processOrderData(textData); // Process the data
                 }).catch(err => {
                    console.error("[Fetch Intercept] 读取响应体时出错:", err);
                     lastErrorMessage = "读取Fetch响应体失败: " + err.message;
                     updateButtonStatus(false, "读取响应失败");
                 });
            }
        }).catch(err => {
            console.error("[Fetch Intercept] Fetch 请求失败:", err);
             lastErrorMessage = "Fetch请求失败: " + err.message;
             updateButtonStatus(false, "请求失败");
        });

        return fetchPromise;
    };


    // --- 核心逻辑:拦截 XMLHttpRequest 请求 ---
    const originalXhrOpen = XMLHttpRequest.prototype.open;
    const originalXhrSend = XMLHttpRequest.prototype.send;

    XMLHttpRequest.prototype.open = function(method, url) {
        this._requestMethod = method; // Store method and url on the xhr object itself
        this._requestUrl = url;
        console.log(`[XHR Intercept] Open: ${method} ${url}`); // Log XHR open
        return originalXhrOpen.apply(this, arguments);
    };

    XMLHttpRequest.prototype.send = function() {
        const xhr = this; // Capture 'this' context
        console.log(`[XHR Intercept] Send: ${xhr._requestUrl}`);

        const originalOnReadyStateChange = xhr.onreadystatechange;

        xhr.onreadystatechange = function() {
             // Log all readyState changes for debugging
             // console.log(`[XHR Intercept] ReadyStateChange: ${xhr._requestUrl}, State: ${xhr.readyState}, Status: ${xhr.status}`);

            if (xhr.readyState === 4) { // DONE
                console.log(`[XHR Intercept] ReadyState 4 for: ${xhr._requestUrl}, Status: ${xhr.status}`); // Log when done
                if (xhr._requestUrl && xhr._requestUrl.includes('/shopApi/Order/list')) {
                     if (xhr.status === 200) {
                         console.log("[XHR Intercept] 检测到目标订单 API 响应 (status 200):", xhr._requestUrl);
                         processOrderData(xhr.responseText); // Process the response text
                     } else {
                         console.warn(`[XHR Intercept] 目标 API 响应状态非 200: ${xhr.status}`);
                         lastErrorMessage = `目标 API [${xhr._requestUrl}] 响应状态: ${xhr.status}`;
                         updateButtonStatus(false, `API状态 ${xhr.status}`);
                     }
                }
            }

            // Call original listener if it exists
            if (originalOnReadyStateChange) {
                try {
                    originalOnReadyStateChange.apply(xhr, arguments);
                } catch (err) {
                     console.error("[XHR Intercept] 调用原始 onreadystatechange 出错:", err);
                }
            }
        };

        try {
             return originalXhrSend.apply(this, arguments);
        } catch (err) {
             console.error("[XHR Intercept] 调用原始 send 出错:", err);
             lastErrorMessage = "调用原始XHR send失败: " + err.message;
             updateButtonStatus(false, "XHR Send错误");
             throw err; // Re-throw error
        }
    };


    // --- 创建和管理按钮 ---
    function createButton() {
        if (document.getElementById('show-stats-button')) return; // Avoid creating duplicates

        statsButton = document.createElement('button');
        statsButton.id = 'show-stats-button';
        statsButton.textContent = '脚本加载中...'; // Initial text
        statsButton.disabled = true;
        statsButton.addEventListener('click', showStatsModal);

        // Basic styles (GM_addStyle applied later)
         statsButton.style.position = 'fixed';
         statsButton.style.bottom = '60px';
         statsButton.style.right = '10px';
         statsButton.style.zIndex = '9998';
         statsButton.style.padding = '8px 15px';
         statsButton.style.backgroundColor = '#6c757d'; // Grey loading color
         statsButton.style.color = 'white';
         statsButton.style.border = 'none';
         statsButton.style.borderRadius = '5px';
         statsButton.style.cursor = 'not-allowed';
         statsButton.style.fontSize = '14px';
         statsButton.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
         statsButton.style.transition = 'background-color 0.3s, color 0.3s, opacity 0.3s';


        document.body.appendChild(statsButton);
        // Update text slightly after adding to DOM to confirm execution
        setTimeout(() => {
            // 只有在数据尚未就绪时才更新为“等待数据”
            if (!dataReady && statsButton && statsButton.textContent === '脚本加载中...') {
                 statsButton.textContent = '等待数据...';
                 statsButton.style.cursor = 'not-allowed';
                 statsButton.style.backgroundColor = '#ffc107'; // Yellow for waiting
                 statsButton.style.color = '#333';
            }
        }, 500); // Give it half a second
    }

    // --- 创建和管理弹窗 ---
    function createModal() {
        if (document.getElementById('stats-overlay')) return; // Avoid duplicates

        modalOverlay = document.createElement('div');
        modalOverlay.id = 'stats-overlay';
        // Styles added via GM_addStyle

        const modalBox = document.createElement('div');
        modalBox.id = 'stats-modal';

        const closeButton = document.createElement('button');
        closeButton.id = 'stats-close-btn';
        closeButton.textContent = '×';
        closeButton.addEventListener('click', hideStatsModal);

        const modalContent = document.createElement('div');
        modalContent.id = 'stats-content';
        modalContent.innerHTML = '请稍候...';

        modalBox.appendChild(closeButton);
        modalBox.appendChild(modalContent);
        modalOverlay.appendChild(modalBox);

        modalOverlay.addEventListener('click', function(event) {
            if (event.target === modalOverlay) {
                hideStatsModal();
            }
        });

        document.body.appendChild(modalOverlay);
    }

    function showStatsModal() {
        const contentElement = document.getElementById('stats-content');
        if (!contentElement) {
            console.error("无法找到弹窗内容元素 #stats-content");
            return;
        };

        if (dataReady) {
            contentElement.innerHTML = `
                <h3>订单统计 (最近加载)</h3>
                <p>订单数量: <strong>${latestCount}</strong> 单</p>
                <p>订单总额: <strong>¥ ${latestAmountStr}</strong></p>
                <p class="api-total">API报告的总订单数(所有分页): <strong>${latestApiTotal}</strong></p>
            `;
        } else {
             contentElement.innerHTML = `
                <h3>订单统计</h3>
                <p>尚未成功加载和处理订单数据。</p>
                ${lastErrorMessage ? `<p style="color:red; font-weight:bold;">最后错误:${lastErrorMessage}</p>` : ''}
                <p>请尝试刷新页面、进行搜索或点击筛选/翻页来触发数据加载。</p>
                <p style="font-size:0.8em; color:#999;">(请同时检查浏览器控制台 F12 获取详细信息)</p>
            `;
        }
        if (modalOverlay) {
            modalOverlay.style.display = 'flex';
        }
    }

    function hideStatsModal() {
        if (modalOverlay) {
            modalOverlay.style.display = 'none';
        }
    }

    // --- 添加样式 ---
    function addStyles() {
        GM_addStyle(`
            #show-stats-button:disabled {
                opacity: 0.7;
            }
            #show-stats-button:not(:disabled):hover {
                filter: brightness(90%);
            }
            /* Modal Styles */
            #stats-overlay {
                position: fixed; top: 0; left: 0; width: 100%; height: 100%;
                background-color: rgba(0, 0, 0, 0.6); display: none; /* Initial hide */
                justify-content: center; align-items: center; z-index: 10000;
            }
            #stats-modal {
                background-color: white; padding: 25px; border-radius: 8px;
                box-shadow: 0 5px 15px rgba(0,0,0,0.3); min-width: 280px;
                max-width: 90%; position: relative; text-align: left; line-height: 1.6;
            }
            #stats-close-btn {
                position: absolute; top: 5px; right: 10px; background: none;
                border: none; font-size: 24px; font-weight: bold;
                color: #888; cursor: pointer; padding: 5px; line-height: 1;
            }
            #stats-close-btn:hover { color: #333; }
            #stats-content h3 {
                 margin-top: 0; margin-bottom: 15px; color: #333;
                 border-bottom: 1px solid #eee; padding-bottom: 10px;
             }
             #stats-content p { margin: 8px 0; color: #555; }
             #stats-content strong { color: #0056b3; font-weight: bold; }
             #stats-content .api-total {
                 margin-top: 15px; padding-top: 10px;
                 border-top: 1px solid #eee; font-size: 0.9em; color: #6c757d;
             }
        `);
    }

    // --- 初始化 ---
    // Waits for body element before adding UI elements
    function onBodyExists(callback) {
        if (document.body) {
            callback();
        } else {
            // Fallback for browsers that might not support observing documentElement for body add
            const observer = new MutationObserver(mutations => {
                if (document.body) {
                    observer.disconnect();
                    callback();
                }
            });
             // Observe documentElement or document directly for body addition
            observer.observe(document.documentElement || document, { childList: true, subtree: true });
        }
    }

    onBodyExists(() => {
         console.log("Body element存在, 创建UI元素。");
         addStyles(); // Add styles first
         createButton();
         createModal();
         console.log("UI元素创建完毕。拦截器应该已设置。");
    });

})();