闲管家一键上架|闲鱼图片下载|闲鱼商品信息提取|导出闲鱼猜你喜欢(修复优化版)

在商品页面复制商品信息,下载商品详情图片,并在闲鱼管家后台上架页面插入“一键填充”按钮。在闲鱼商详页导出猜你喜欢的数据到Excel,支持动态数据。

// ==UserScript==
// @name         闲管家一键上架|闲鱼图片下载|闲鱼商品信息提取|导出闲鱼猜你喜欢(修复优化版)
// @namespace    http://tampermonkey.net/
// @version      3.38
// @description  在商品页面复制商品信息,下载商品详情图片,并在闲鱼管家后台上架页面插入“一键填充”按钮。在闲鱼商详页导出猜你喜欢的数据到Excel,支持动态数据。
// @author       GitHub:Dolphin-QvQ    https://github.com/Dolphin-QvQ
// @match        https://h5.m.goofish.com/item?id=*
// @match        https://www.goofish.com/item*
// @match        https://goofish.pro/*
// @match        https://www.goofish.pro/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=goofish.pro
// @grant        GM_setValue
// @grant        GM_getValue
// @require      https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.all.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js
// @license      GPL
// ==/UserScript==

(function () {
    "use strict";

    const unsafeWindow = window.unsafeWindow || window;

    if (!window.collectedData) {
        window.collectedData = [];
    }

    interceptRequests();

    if (window.location.href.includes("https://www.goofish.com/item")) {
        initProductPageFunctions();
    }

    if (window.location.hostname.includes("goofish.pro")) {
        initGoofishProPage();
    }

    function showToast(message, type = "success") {
        Swal.fire({
            toast: true,
            position: "top-end",
            icon: type,
            title: message,
            showConfirmButton: false,
            timer: 1500,
        });
    }

    function downloadImage(url, fileName) {
        fetch(url)
            .then(response => response.blob())
            .then(blob => {
                const a = document.createElement("a");
                a.href = URL.createObjectURL(blob);
                a.download = `${fileName}.jpg`;
                a.click();
                URL.revokeObjectURL(a.href);
            })
            .catch(error => console.error("图片下载失败:", error));
    }

    function getRandomNumber(min, max) {
        const randomInt = Math.floor(Math.random() * (max / 10 - min / 10 + 1)) + min / 10;
        return randomInt * 10;
    }

    function getPrice(priceStr) {
        const cleanedStr = priceStr.replace(/[¥\n]/g, "");
        const numbers = cleanedStr.match(/\d+(\.\d+)?/g);
        if (numbers === null) {
            return null;
        }
        const prices = numbers.map(Number);
        return Math.max(...prices);
    }

    function initProductPageFunctions() {
        addCopyAndDownloadButton();
        addConversionRateDisplay();
        exportGoodsList();
    }

    function addCopyAndDownloadButton() {
        const observer = new MutationObserver((mutations, obs) => {
            const slideContainer = document.querySelector(".slick-slider");
            if (slideContainer) {
                let goofishImages = document.querySelectorAll("div.slick-slide:not(.slick-cloned)").length;
                let button = document.querySelector("#copy-download-btn");
                if (!button) {
                    button = document.createElement("button");
                    button.id = "copy-download-btn";
                    button.style.position = "fixed";
                    button.style.bottom = "20px";
                    button.style.left = "20px";
                    button.style.zIndex = 1000;
                    button.style.padding = "10px 20px";
                    button.style.backgroundColor = "#28a745";
                    button.style.color = "white";
                    button.style.border = "none";
                    button.style.borderRadius = "5px";
                    button.style.cursor = "pointer";
                    button.addEventListener("click", () => {
                        copyGoods();
                        downloadImages();
                    });
                    document.body.appendChild(button);
                }
                button.innerHTML = `复制商品信息 & 下载图片( <b> ${goofishImages} </b>)`;
                if (goofishImages > 0) {
                    obs.disconnect();
                }
            }
        });

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

    // 商品名和描述的提取逻辑
    function copyGoods() {
        const descElement = document.querySelector('span[class^="desc--"]');
        const priceElements = document.querySelectorAll("[class*='price']")[0];

        if (descElement && priceElements) {
            const detailText = descElement.innerText.trim();
            let productName, productDescription;
            const lineBreakIndex = detailText.indexOf("\n");
            if (lineBreakIndex !== -1) {
                productName = detailText.substring(0, lineBreakIndex).trim();
                productDescription = detailText;
            } else {
                productName = detailText;
                productDescription = detailText;
            }

            // 裁剪商品名到30个中文字以内
            const maxTitleLength = 30;
            productName = productName.slice(0, maxTitleLength);

            // 存储商品信息
            GM_setValue("productName", productName);
            GM_setValue("productPrice", getPrice(priceElements.innerText.trim()));
            GM_setValue("productDescription", productDescription);

            console.log("提取的商品名:", productName);
            showToast("商品信息已复制");
        } else {
            console.error("未找到商品详情或价格元素");
            showToast("未找到商品详情或价格元素", "error");
        }
    }

    function downloadImages() {
        const goofishImageDivs = document.querySelectorAll("div.slick-slide:not(.slick-cloned)");
        const goofishImages = Array.from(goofishImageDivs).map(div => div.querySelector("img"));
        const filteredGoofishImages = goofishImages.filter(img => img !== null);

        filteredGoofishImages.forEach((img, index) => {
            setTimeout(() => {
                let src = img.src.replace(/_webp$/, "");
                downloadImage(src, `${index + 1}`);
            }, index * 100);
        });
        showToast("图片正在开始下载");
    }

    function initGoofishProPage() {
        const button = document.createElement("button");
        button.innerText = "一键填充";
        button.style.position = "fixed";
        button.style.bottom = "10px";
        button.style.left = "150px";
        button.style.zIndex = 1000;
        button.style.padding = "10px 20px";
        button.style.backgroundColor = "#28a745";
        button.style.color = "white";
        button.style.border = "none";
        button.style.borderRadius = "5px";
        button.style.cursor = "pointer";

        document.body.appendChild(button);

        button.addEventListener("click", () => {
            if (!window.location.href.includes("/sale/product/add")) {
                window.location.href = "https://goofish.pro/sale/product/add?from=%2Fon-sale";
                setTimeout(() => fillProductInfo(), 1500);
                return;
            } else {
                fillProductInfo();
            }
        });
    }

    function fillProductInfo() {
        const productName = GM_getValue("productName", "");
        const productDescription = GM_getValue("productDescription", "") || productName;
        let prict = GM_getValue("productPrice");
        let productPrice = 100;
        if (prict < 2) {
            productPrice = 1.9;
        } else {
            productPrice = parseFloat(GM_getValue("productPrice", "100")) - 0.1;
        }

        if (!productName || !productDescription || isNaN(productPrice)) {
            showToast("请先去闲鱼详情页复制商品信息", "warning");
            return;
        }

        setTimeout(() => {
            const shopList = document.querySelectorAll("ul.auth-list li");
            if (shopList.length > 1) {
                // 店铺选择优化:当有selected-icon时不选择,否则点击第一个未选中图标
                const selectedIcon = document.querySelector(".selected-icon");
                const unSelectedIconElements = document.querySelector(".un-selected-icon");

                // 如果没有选中的店铺且存在未选中的店铺图标,则点击选中第一个
                if (!selectedIcon && unSelectedIconElements) {
                    unSelectedIconElements.click();
                }
            } else {
                console.error("请先创建闲鱼店铺");
                showToast("请先创建闲鱼店铺", "error");
            }

            setTimeout(() => {
                const inputElement = document.querySelector('input[placeholder="请输入商品标题,最多允许输入30个汉字"]');
                if (inputElement) {
                    inputElement.value = productName;
                    const event = new Event("input", { bubbles: true });
                    inputElement.dispatchEvent(event);
                } else {
                    console.error("未找到商品标题输入框");
                    showToast("未找到商品标题输入框", "error");
                }

                const descriptionElements = document.querySelector('textarea[placeholder="请输入商品描述"]');
                if (descriptionElements) {
                    descriptionElements.value = productDescription;
                    const event = new Event("input", { bubbles: true });
                    descriptionElements.dispatchEvent(event);
                } else {
                    console.error("未找到商品描述输入框");
                    showToast("未找到商品描述输入框", "error");
                }

                const inputElements = document.querySelectorAll('input[placeholder="¥ 0.00"]');
                if (inputElements[0]) {
                    inputElements[0].value = getRandomNumber(200, 300);
                    const event = new Event("input", { bubbles: true });
                    inputElements[0].dispatchEvent(event);
                } else {
                    console.error("未找到价格输入框");
                    showToast("未找到价格输入框", "error");
                }
                if (inputElements[1]) {
                    inputElements[1].value = productPrice.toFixed(2);
                    const event = new Event("input", { bubbles: true });
                    inputElements[1].dispatchEvent(event);
                } else {
                    console.error("未找到价格输入框");
                    showToast("未找到价格输入框", "error");
                }

                showToast("商品信息已填充");
            }, 500);
        }, 500);
    }

    function addConversionRateDisplay() {
        const observer = new MutationObserver((mutations, obs) => {
            const wantElements = [
                document.querySelectorAll("[class*='want']")[0],
                document.querySelector(".want--mVAXJTGv"),
                document.querySelector("[data-spm='want']")
            ];

            const spanElement = wantElements.find(el => el !== undefined && el !== null);

            if (spanElement) {
                const textContent = spanElement.textContent.trim();
                let wantText = "0人想要";
                let viewText = "0浏览";

                if (textContent.includes("人想要") && textContent.includes("浏览")) {
                    [wantText, viewText] = textContent.split(" ");
                } else if (textContent.includes("浏览")) {
                    viewText = textContent;
                }

                const wantNumber = parseInt(wantText.replace("人想要", "").trim(), 10) || 0;
                const viewNumber = parseInt(viewText.replace("浏览", "").trim(), 10) || 0;

                let rate = 0;
                if (wantNumber != 0 || viewNumber != 0) {
                    rate = wantNumber / viewNumber;
                }
                const conversionRate = (rate * 100).toFixed(0);
                const conversionRateText = conversionRate + "%";

                const statsDiv = document.createElement("div");
                statsDiv.style.position = "fixed";
                statsDiv.style.bottom = "63px";
                statsDiv.style.left = "20px";
                statsDiv.style.borderRadius = "6px";
                statsDiv.style.color = "white";
                statsDiv.style.padding = "10px";
                statsDiv.style.zIndex = "1000";
                statsDiv.style.fontSize = "14px";
                statsDiv.style.backgroundColor = conversionRate > 7 ? "#93ab9b" : "rgb(211 131 131)";

                const conversionRateSpan = document.createElement("b");
                conversionRateSpan.textContent = conversionRateText;
                conversionRateSpan.style.backgroundColor = conversionRate > 7 ? "green" : "red";
                conversionRateSpan.style.padding = "2px 4px";
                conversionRateSpan.setAttribute("id", "conversion-rate");

                statsDiv.innerHTML = `
想要数 : <b id="want-num">${wantNumber}</b><br>
浏览量 : <b id="view-num">${viewNumber}</b><br>
转化率 : `;
                statsDiv.appendChild(conversionRateSpan);

                document.body.appendChild(statsDiv);

                if (conversionRate < 7) {
                    console.log(`转化率太低了 ${conversionRate}%`);
                }

                obs.disconnect();
            }
        });

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

        setTimeout(() => {
            observer.disconnect();
            const existingStats = document.querySelector("#conversion-rate");
            if (!existingStats) {
                console.log("未能找到转化率相关元素");
            }
        }, 5000);
    }

    function interceptRequests() {
        const originalXHR = unsafeWindow.XMLHttpRequest;
        unsafeWindow.XMLHttpRequest = function () {
            const xhr = new originalXHR();

            const originalOpen = xhr.open;
            xhr.open = function (method, url, ...rest) {
                this._url = url;
                return originalOpen.apply(this, [method, url, ...rest]);
            };

            const originalSend = xhr.send;
            xhr.send = function (...args) {
                this.addEventListener("load", function () {
                    if (this._url.includes("h5api.m.goofish.com/h5/mtop.taobao.idle.pc.detail")) {
                        try {
                            const data = JSON.parse(this.responseText);
                            if (data?.data?.itemDO?.desc) {
                                let productDescription = data.data.itemDO.desc;
                                GM_setValue("productDescription", productDescription);
                            }
                        } catch (error) {
                            console.error("解析 XHR 响应时发生错误:", error);
                        }
                    }

                    if (this._url.includes("h5api.m.goofish.com/h5/mtop.taobao.idle.item.web.recommend.list")) {
                        try {
                            const data = JSON.parse(this.responseText);
                            if (data && Array.isArray(data.data?.cardList)) {
                                const tempData = data.data.cardList.filter(item => {
                                    if (item && item.cardData && item.cardData.itemId) {
                                        const price = parseFloat(item.cardData.price);
                                        return price > 0;
                                    }
                                    return false;
                                });

                                window.collectedData.push(...tempData);

                                const uniqueItems = [];
                                const itemIdSet = new Set();
                                for (const item of window.collectedData) {
                                    const itemId = item.cardData?.itemId;
                                    if (itemId && !itemIdSet.has(itemId)) {
                                        uniqueItems.push(item);
                                        itemIdSet.add(itemId);
                                    }
                                }
                                window.collectedData = uniqueItems;
                                updateDataCount();
                            }
                        } catch (error) {
                            console.error("解析 XHR 响应时发生错误:", error);
                        }
                    }
                });
                return originalSend.apply(this, args);
            };

            return xhr;
        };
    }

    repleaceUrl();
    function repleaceUrl() {
        const currentUrl = window.location.href;
        if (currentUrl.startsWith("https://www.goofish.pro/")) {
            const newUrl = currentUrl.replace("https://www.", "https://");
            window.location.replace(newUrl);
        }
    }

    function exportGoodsList() {
        insertDownloadButton();
        setInterval(updateDataCount, 1000);
    }

    function updateDataCount() {
        const countElement = document.getElementById("data-count");
        if (countElement && window.collectedData) {
            countElement.innerText = window.collectedData.length;
        }
    }

    function insertDownloadButton() {
        if (document.querySelector("#export-goods-btn")) {
            return;
        }

        const button = document.createElement("button");
        button.id = "export-goods-btn";
        button.innerHTML = `导出 [猜你喜欢] 商品 (<b id="data-count">0</b>)`;
        button.style.position = "fixed";
        button.style.width = "240px";
        button.style.left = "50%";
        button.style.marginLeft = "-120px";
        button.style.bottom = "20px";
        button.style.zIndex = 9999;
        button.style.padding = "10px 20px";
        button.style.backgroundColor = "rgb(40, 167, 69)";
        button.style.color = "#FFFFFF";
        button.style.border = "none";
        button.style.borderRadius = "5px";
        button.style.cursor = "pointer";
        button.addEventListener("click", downloadExcel);
        document.body.appendChild(button);
    }

    function timestampToFormattedDate(timestamp) {
        if (!timestamp) return "";
        var date = new Date(parseInt(timestamp));
        var year = date.getFullYear();
        var month = date.getMonth() + 1;
        var day = date.getDate();
        month = month < 10 ? "0" + month : month;
        day = day < 10 ? "0" + day : day;
        return `${year}-${month}-${day}`;
    }

    function downloadExcel() {
        if (!window.collectedData || window.collectedData.length === 0) {
            alert("没有数据可导出");
            return;
        }

        const workbook = XLSX.utils.book_new();
        const worksheetData = window.collectedData
            .map(item => {
                const originalPrice = parseFloat(item.cardData.price) || 0;
                const adjustedPrice = (originalPrice - 0.1).toFixed(2);
                const sellerName = item.cardData?.user?.userNick || "未知卖家";
                return {
                    链接: {
                        f: `HYPERLINK("https://www.goofish.com/item?id=${item.cardData.itemId}", "https://www.goofish.com/item?id=${item.cardData.itemId}")`,
                    },
                    标题: {
                        f: `HYPERLINK("https://www.goofish.com/item?id=${item.cardData.itemId}", "${item.cardData.title}")`,
                    },
                    店家名: sellerName,
                    想要数: parseInt(item.cardData.clickParam.args.wantNum, 10) || "",
                    价格: { t: 'n', v: parseFloat(adjustedPrice) },
                    城市: item.cardData.area || "",
                    发布日期: timestampToFormattedDate(item.cardData?.clickParam?.args?.publishTime),
                };
            })
            .sort((a, b) => b.想要数 - a.想要数);

        const worksheet = XLSX.utils.json_to_sheet(worksheetData);
        const priceColumn = XLSX.utils.decode_col('E');
        for (let i = 1; i <= worksheetData.length; i++) {
            const cellAddress = { r: i, c: priceColumn };
            const cellRef = XLSX.utils.encode_cell(cellAddress);
            if (!worksheet[cellRef]) continue;
            worksheet[cellRef].z = '0.00';
        }

        XLSX.utils.book_append_sheet(workbook, worksheet, "猜你喜欢");
        const workbookOut = XLSX.write(workbook, { bookType: "xlsx", type: "array" });
        const blob = new Blob([workbookOut], { type: "application/octet-stream" });
        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = url;
        a.download = "猜你喜欢商品.xlsx";
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }
})();