Taobao orders export 淘宝订单快递导出

Based on 淘宝买家订单导出 by CMA, added tracking number on top of it to the export data. Instructions are listed on GitHub

// ==UserScript==
// @name         Taobao orders export 淘宝订单快递导出
// @namespace    http://tampermonkey.net/
// @version      0.1.0
// @description  Based on 淘宝买家订单导出 by CMA, added tracking number on top of it to the export data. Instructions are listed on GitHub
// @author       Trizzy33
// @include      https://buyertrade.taobao*
// @require      https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js
// @grant        GM_xmlhttpRequest
// @license      MIT
// ==/UserScript==


//github https://github.com/Trizzy33/taobaoTrackingNumberExport/blob/main/script


let orderList = [];
let state = {
    orderList: [],
    isProcessing: false,
    shouldStop: false,
    currentWindow: null
};

function addButton(parent, onClick, text) {
    const btn = document.createElement("input");
    btn.type = "button";
    btn.value = text;
    Object.assign(btn.style, {
        padding: "10px 20px",
        margin: "10px",
        backgroundColor: "#409EFF",
        border: "none",
        borderRadius: "8px",
        boxShadow: "0 2px 6px rgba(0,0,0,0.2)",
        color: "#fff",
        fontWeight: "bold",
        cursor: "pointer",
        transition: "all 0.2s ease"
    });
    btn.onmouseenter = () => { btn.style.backgroundColor = "#66b1ff"; };
    btn.onmouseleave = () => { btn.style.backgroundColor = "#409EFF"; };
    btn.onclick = onClick;
    parent.insertBefore(btn, parent.firstChild);
}

if (/buyertrade\.taobao/.test(window.location.href)) {
    const main = document.getElementById("J_bought_main");
    addButton(main, addCurrentPageOrdersToList, "添加本页订单");
    addButton(main, exportOrders, "导出订单");
    addButton(main, stopScript, "停止脚本");
}

function toCsv(header, data, filename) {
    let rows = '\uFEFF' + header.join(',') + '\n';
    for (let order of data) {
        // 防止 Excel 科学计数:加 \t
        order[0] = '\t' + order[0]; // 订单号
        order[7] = order[7] ? '\t' + order[7] : ''; // 运单号
        rows += order.join(',') + '\n';
    }
    const blob = new Blob([rows], { type: 'text/csv;charset=utf-8;' });
    const url = URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = url;
    link.download = filename + ".csv";
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
}

function addCurrentPageOrdersToList() {
    if (state.isProcessing) return;
    state.isProcessing = true;
    state.shouldStop = false;
    const orders = document.querySelectorAll(".js-order-container");
    processOrders(orders);
}

function exportOrders() {
    const header = ["订单号", "商品名称", "商品链接", "单价", "数量", "实付款", "状态", "快递单号"];
    toCsv(header, orderList, "淘宝订单导出");
}

function stopScript() {
    state.shouldStop = true;
    state.isProcessing = false;
    if (state.currentWindow && !state.currentWindow.closed) {
        state.currentWindow.close();
    }
    if (state.orderList.length > 0) exportOrders();
}

function processOrders(orders, current = 0, results = []) {
    if (current >= orders.length || state.shouldStop) {
        console.table(results);
        return results;
    }

    const order = orders[current];
    const id = order.getAttribute('data-reactid')?.match(/order-(\d+)/)?.[1];

    const statusText = order.querySelector("span[data-reactid*='.$5.0.0.0']")?.textContent || '';
    if (statusText.includes("交易成功")) {
        return processOrders(orders, current + 1, results); // skip finished orders
    }

    processOrderItems(order, id, 0, [], (orderResults) => {
        orderList.push(orderResults);
        setTimeout(() => processOrders(orders, current + 1, results), 500);
    });
}

function processOrderItems(order, id, index = 0, productNames = [], callback) {
    try {
        let productQuery = order.querySelector("span[data-reactid='.0.7:$order-" + id + ".$" + id + ".0.1:1:0.$" + index + ".$0.0.1.0.0.1']");
        if (productQuery == null) {
            // 没有更多商品,结束处理,返回合并名称
            const joinedNames = productNames.join('||') || '';
            const queries = {
                itemUrl: order.querySelector("a[href]"),
                price: order.querySelector("span[data-reactid='.0.7:$order-" + id + ".$" + id + ".0.1:1:0.$0.$1.0.1.1']"),
                count: order.querySelector("p[data-reactid='.0.7:$order-" + id + ".$" + id + ".0.1:1:0.$0.$2.0.0']"),
                actualPay: order.querySelector("span[data-reactid='.0.7:$order-" + id + ".$" + id + ".0.1:1:0.$0.$4.0.0.2.0.1']"),
                status: order.querySelector("span[data-reactid='.0.7:$order-" + id + ".$" + id + ".0.1:1:0.$0.$5.0.0.0']"),
                tracking: order.querySelector("#viewLogistic")
            };

            if (queries.status?.textContent === '交易成功') {
                callback(); // 跳过
                return;
            }

            const orderItem = [
                id,
                joinedNames,
                queries.itemUrl?.href || '',
                parseFloat(queries.price?.textContent || 0),
                queries.count?.textContent || '',
                queries.actualPay?.textContent || '',
                queries.status?.textContent || ''
            ];

            if (queries.tracking) {
                getTrackingNumberViaHover(queries.tracking).then(trackingNumber => {
                    orderItem.push(trackingNumber);
                    callback(orderItem);
                });
            } else {
                orderItem.push('0');
                callback(orderItem);
            }

            return;
        }

        // 有商品,记录商品名后递归
        productNames.push(productQuery.textContent.replace(/,/g, ","));
        processOrderItems(order, id, index + 1, productNames, callback);
    } catch (e) {
        console.error(`处理订单 ${id} 时出错`, e);
        callback([]);
    }
}

async function getTrackingNumberViaHover(el) {
    return new Promise((resolve) => {
        if (state.shouldStop) return resolve('0');
        ['mouseenter', 'mouseover'].forEach(e => el.dispatchEvent(new MouseEvent(e, { bubbles: true })));
        const observer = new MutationObserver((mutations, obs) => {
            for (const m of mutations) {
                for (const node of m.addedNodes) {
                    const tooltip = node.querySelector?.('.logistics-info-mod__header___gNRGw');
                    if (tooltip) {
                        const match = tooltip.textContent.match(/\d{13,}/);
                        if (match) {
                            obs.disconnect();
                            return resolve(match[0]);
                        }
                    }
                }
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
        setTimeout(() => {
            observer.disconnect();
            resolve('0');
        }, 4000);
    });
}