Goofish闲鱼商品图片自动下载器

简约的goofish商品信息面板

// ==UserScript==
// @name         Goofish闲鱼商品图片自动下载器
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  简约的goofish商品信息面板
// @author       雷锋[email protected]
// @match        https://www.goofish.com/*
// @match        https://goofish.com/*
// @match        https://*.goofish.com/*
// @include      https://www.goofish.com/*
// @include      https://goofish.com/*
// @include      https://*.goofish.com/*
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        none
// @license      BSD-3-Clause
// @run-at       document-start
// @noframes
// ==/UserScript==

(function() {
    'use strict';
    
    // 检查脚本环境
    if (typeof GM_info !== 'undefined') {
        // Tampermonkey环境
    } else {
        // 非Tampermonkey环境
    }
    
    // 检查页面加载状态
    function checkPageReady() {
        if (document.readyState === 'loading') {
            setTimeout(checkPageReady, 100);
            return;
        }
    }
    checkPageReady();
    
    let itemData = null;
    let imageInfos = [];
    let panel = null;
    
    // 简化的下载设置
    const downloadSettings = {
        format: 'jpg' // 固定使用JPG格式
    };
    
    // 创建迷你面板
    function createMiniPanel() {
        if (panel) panel.remove();
        
        panel = document.createElement('div');
        panel.id = 'goofish-mini-panel';
        panel.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            width: 340px;
            background: rgba(255, 255, 255, 0.98);
            border: 1px solid rgba(0, 0, 0, 0.1);
            border-radius: 8px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08);
            z-index: 10000;
            font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif;
            font-size: 14px;
            opacity: 0.15;
            transition: opacity 0.3s ease;
            cursor: move;
            backdrop-filter: blur(12px);
            overflow: hidden;
            will-change: transform;
        `;
        
        // 悬停显示
        panel.addEventListener('mouseenter', () => panel.style.opacity = '1');
        panel.addEventListener('mouseleave', () => panel.style.opacity = '0.15');
        
        // 拖拽功能 - 高性能版本
        let dragging = false;
        let startX, startY, startLeft, startTop;
        
        panel.addEventListener('mousedown', (e) => {
            // 只允许在标题栏拖拽,排除关闭按钮
            if (e.target.closest('[data-drag-handle]') && !e.target.closest('button[onclick*="closest"]')) {
                dragging = true;
                startX = e.clientX;
                startY = e.clientY;
                startLeft = panel.offsetLeft;
                startTop = panel.offsetTop;
                panel.style.cursor = 'grabbing';
                
                // 拖拽时禁用所有可能影响性能的CSS效果
                panel.style.transition = 'none';
                panel.style.backdropFilter = 'none';
                panel.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.2)'; // 简化阴影
                
                e.preventDefault();
            }
        });
        
        document.addEventListener('mousemove', (e) => {
            if (dragging) {
                e.preventDefault();
                // 直接设置位置,不使用requestAnimationFrame避免延迟
                const newLeft = startLeft + e.clientX - startX;
                const newTop = startTop + e.clientY - startY;
                
                panel.style.left = newLeft + 'px';
                panel.style.top = newTop + 'px';
                panel.style.right = 'auto';
            }
        });
        
        document.addEventListener('mouseup', () => {
            if (dragging) {
                dragging = false;
                panel.style.cursor = 'move';
                
                // 恢复所有CSS效果
                panel.style.transition = 'opacity 0.3s ease';
                panel.style.backdropFilter = 'blur(12px)';
                panel.style.boxShadow = '0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08)';
            }
        });
        
        updatePanel();
        document.body.appendChild(panel);
    }
    
    // 更新面板内容
    function updatePanel() {
        if (!panel) return;
        
        if (!itemData || !imageInfos.length) {
            panel.innerHTML = `
                <div style="padding: 10px; background: #007bff; color: white; border-radius: 8px 8px 0 0; text-align: center; font-weight: bold;">
                    🛍️ Goofish商品信息
                </div>
                <div style="padding: 20px; text-align: center; color: #666;">
                    等待获取商品数据...
                </div>
            `;
            return;
        }
        
        const title = itemData.title || '未知商品';
        const price = itemData.soldPrice || '未知';
        const desc = itemData.desc || '暂无描述';
        
        // 修复图片URL(解决Mixed Content问题)
        function fixImageUrl(url) {
            if (!url) return '';
            
            // 如果是相对路径,添加HTTPS协议
            if (url.startsWith('//')) {
                return 'https:' + url;
            }
            
            // 如果是http,强制改为https(解决Mixed Content问题)
            if (url.startsWith('http://')) {
                const httpsUrl = url.replace('http://', 'https://');
                return httpsUrl;
            }
            
            return url;
        }
        
        // 九宫格图片
        const imageGrid = imageInfos.slice(0, 9).map((img, index) => {
            const fixedUrl = fixImageUrl(img.url);
            
            return `
            <div style="
                aspect-ratio: 1;
                background: #f0f0f0;
                border-radius: 4px;
                overflow: hidden;
                cursor: pointer;
                position: relative;
                transition: transform 0.2s ease;
            " 
            onmouseover="this.style.transform='scale(1.1)'"
            onmouseout="this.style.transform='scale(1)'"
            onclick="downloadImage('${img.url}', '${index + 1}')"
            title="点击下载">
                <img src="${fixedUrl}" 
                     style="width: 100%; height: 100%; object-fit: cover;"
                     onerror="this.style.display='none'; this.parentElement.innerHTML='<div style=\\'display: flex; align-items: center; justify-content: center; height: 100%; background: #f0f0f0; color: #999; font-size: 10px;\\'>图片加载失败</div>'"
                     onload=""
                     loading="lazy">
                <div style="
                    position: absolute;
                    bottom: 1px;
                    right: 1px;
                    background: rgba(0,0,0,0.7);
                    color: white;
                    padding: 1px 3px;
                    border-radius: 2px;
                    font-size: 8px;
                ">${index + 1}</div>
                <div style="
                    position: absolute;
                    top: 2px;
                    right: 2px;
                    background: rgba(220,53,69,0.8);
                    color: white;
                    width: 16px;
                    height: 16px;
                    border-radius: 50%;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    font-size: 10px;
                    cursor: pointer;
                    opacity: 0;
                    transition: opacity 0.2s ease;
                " 
                onmouseover="this.style.opacity='1'"
                onmouseout="this.style.opacity='0'"
                onclick="event.stopPropagation(); removeImage(${index})"
                title="删除图片">×</div>
            </div>
        `;
        }).join('');
        
        panel.innerHTML = `
            <!-- Win11风格标题栏 -->
            <div data-drag-handle="true" style="
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 8px 12px;
                background: linear-gradient(135deg, #ffe60f, #ffe60f);
                color: white;
                border-radius: 8px 8px 0 0;
                font-weight: 600;
                font-size: 14px;
                user-select: none;
                cursor: move;
            ">
                <div style="display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0;">
                    <span style="font-size: 16px;">🛍️</span>
                    <span style="color: #000; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${title}">
                        ${title}
                    </span>
                </div>
                <div style="display: flex; align-items: center; gap: 4px;">
                    <button onclick="this.closest('#goofish-mini-panel').style.display='none'" 
                            style="
                                width: 20px;
                                height: 20px;
                                background: rgba(255,255,255,0.2);
                                border: none;
                                border-radius: 4px;
                                color: #000;
                                cursor: pointer;
                                display: flex;
                                align-items: center;
                                justify-content: center;
                                font-size: 12px;
                                transition: background 0.2s ease;
                            "
                            onmouseover="this.style.background='rgba(255,255,255,0.3)'"
                            onmouseout="this.style.background='rgba(255,255,255,0.2)'"
                            title="关闭">
                        ✕
                    </button>
                </div>
            </div>
            
            <div style="padding: 16px;">
                <!-- 商品信息 -->
                <div style="margin-bottom: 16px; padding: 12px; background: #f8f9fa; border-radius: 8px; border: 1px solid rgba(0, 0, 0, 0.05);">
                    <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
                        <span style="font-weight: 700; color: #e74c3c; font-size: 20px;">¥${price}</span>
                        <span style="color: #6c757d; font-size: 13px; font-weight: 500;">${imageInfos.length}张</span>
                    </div>
                    <div style="color: #495057; font-size: 13px; line-height: 1.5; max-height: 65px; overflow-y: auto;">
                        ${desc.substring(0, 120)}${desc.length > 120 ? '...' : ''}
                    </div>
                </div>
                
                <!-- 九宫格 -->
                <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; margin-bottom: 16px;">
                    ${imageGrid}
                </div>
                
                <!-- 按钮 -->
                <div style="display: flex; gap: 10px; margin-bottom: 16px;">
                    <button onclick="downloadAll()" style="
                        flex: 1; 
                        padding: 12px 16px; 
                        background: linear-gradient(135deg, #ffe60f, #ffe60f); 
                        color: #000; 
                        border: none; 
                        border-radius: 8px; 
                        cursor: pointer; 
                        font-size: 14px; 
                        font-weight: 600;
                        transition: all 0.2s ease;
                        box-shadow: 0 2px 8px rgba(40, 167, 69, 0.3);
                    " 
                    onmouseover="this.style.transform='translateY(-1px)'; this.style.boxShadow='0 4px 12px rgba(40, 167, 69, 0.4)'"
                    onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 8px rgba(40, 167, 69, 0.3)'">
                        📥 全部下载
                    </button>
                    <button onclick="copyUrls()" style="
                        flex: 1; 
                        padding: 12px 16px; 
                        background: linear-gradient(135deg, #3b3b3b, #3b3b3b); 
                        color: white; 
                        border: none; 
                        border-radius: 8px; 
                        cursor: pointer; 
                        font-size: 14px; 
                        font-weight: 600;
                        transition: all 0.2s ease;
                        box-shadow: 0 2px 8px rgba(108, 117, 125, 0.3);
                    "
                    onmouseover="this.style.transform='translateY(-1px)'; this.style.boxShadow='0 4px 12px rgba(108, 117, 125, 0.4)'"
                    onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 8px rgba(108, 117, 125, 0.3)'">
                        📋 复制链接
                    </button>
                </div>
                
                <!-- 下载状态 -->
                <div id="downloadStatus" style="
                    display: none; 
                    margin-bottom: 12px; 
                    padding: 10px; 
                    background: linear-gradient(135deg, #d4edda, #c3e6cb); 
                    border: 1px solid #b8dacc; 
                    border-radius: 8px; 
                    font-size: 13px; 
                    color: #155724; 
                    text-align: center;
                    font-weight: 500;
                    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
                ">
                    <span id="downloadProgress">准备下载...</span>
                </div>
                
            </div>
        `;
    }
    
    
    // 下载功能 - Tampermonkey兼容版本
    window.downloadImage = async function(url, filename) {
        // 修复Mixed Content问题
        if (url.startsWith('http://')) {
            url = url.replace('http://', 'https://');
        }
        
        try {
            // 方法1:尝试使用GM_xmlhttpRequest(Tampermonkey专用)
            if (typeof GM_xmlhttpRequest !== 'undefined') {
                return new Promise((resolve, reject) => {
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: url,
                        responseType: 'blob',
                        onload: function(response) {
                            try {
                                const blob = response.response;
                                const blobUrl = URL.createObjectURL(blob);
                                
                                const aTag = document.createElement('a');
                                aTag.href = blobUrl;
                                aTag.download = filename.replace(/[<>:"/\\|?*]/g, '_') + '.' + downloadSettings.format;
                                document.body.appendChild(aTag);
                                aTag.click();
                                document.body.removeChild(aTag);
                                URL.revokeObjectURL(blobUrl);
                                
                                resolve();
                            } catch (error) {
                                reject(error);
                            }
                        },
                        onerror: function(error) {
                            reject(error);
                        }
                    });
                });
            }
            
            // 方法2:使用fetch API(可能被CORS阻止)
            const response = await fetch(url, {
                mode: 'cors',
                credentials: 'omit'
            });
            
            if (!response.ok) {
                throw new Error(`请求失败: ${response.status}`);
            }
            
            const blob = await response.blob();
            const blobUrl = URL.createObjectURL(blob);
            
            const aTag = document.createElement('a');
            aTag.href = blobUrl;
            aTag.download = filename.replace(/[<>:"/\\|?*]/g, '_') + '.' + downloadSettings.format;
            document.body.appendChild(aTag);
            aTag.click();
            document.body.removeChild(aTag);
            URL.revokeObjectURL(blobUrl);
            
        } catch (error) {
            // 方法3:最后的备用方案 - 直接链接下载
            try {
                const aTag = document.createElement('a');
                aTag.href = url;
                aTag.download = filename.replace(/[<>:"/\\|?*]/g, '_') + '.' + downloadSettings.format;
                aTag.target = '_blank';
                document.body.appendChild(aTag);
                aTag.click();
                document.body.removeChild(aTag);
            } catch (fallbackError) {
                throw error;
            }
        }
    };
    
    // 高级图片下载功能 - Tampermonkey兼容版本
    window.downloadImageAdvanced = async function(url, filename) {
        // 修复Mixed Content问题
        if (url.startsWith('http://')) {
            url = url.replace('http://', 'https://');
        }
        
        // 检查是否需要格式转换
        const needsConversion = url.includes('.heic') || 
                               (url.includes('.webp') && downloadSettings.format === 'jpg') ||
                               (url.includes('.png') && downloadSettings.format === 'jpg');
        
        if (needsConversion) {
            try {
                let blob;
                
                // 使用GM_xmlhttpRequest获取图片(Tampermonkey专用)
                if (typeof GM_xmlhttpRequest !== 'undefined') {
                    blob = await new Promise((resolve, reject) => {
                        GM_xmlhttpRequest({
                            method: 'GET',
                            url: url,
                            responseType: 'blob',
                            onload: function(response) {
                                resolve(response.response);
                            },
                            onerror: function(error) {
                                reject(error);
                            }
                        });
                    });
                } else {
                    // 使用fetch API
                    const response = await fetch(url, {
                        mode: 'cors',
                        credentials: 'omit'
                    });
                    if (!response.ok) {
                        throw new Error(`请求失败: ${response.status}`);
                    }
                    blob = await response.blob();
                }
                
                // 创建图片元素进行格式转换
                const img = new Image();
                img.crossOrigin = 'anonymous';
                
                return new Promise((resolve, reject) => {
                    img.onload = function() {
                        try {
                            // 创建Canvas进行格式转换
                            const canvas = document.createElement('canvas');
                            const ctx = canvas.getContext('2d');
                            
                            canvas.width = img.width;
                            canvas.height = img.height;
                            
                            // 绘制图片到Canvas
                            ctx.drawImage(img, 0, 0);
                            
                            // 根据设置选择输出格式
                            const mimeType = downloadSettings.format === 'jpg' ? 'image/jpeg' :
                                           downloadSettings.format === 'png' ? 'image/png' : 'image/webp';
                            const quality = downloadSettings.format === 'jpg' ? 0.9 : 1.0;
                            
                            canvas.toBlob(function(convertedBlob) {
                                if (convertedBlob) {
                                    try {
                                        // 使用转换后的blob下载
                                        const downloadUrl = URL.createObjectURL(convertedBlob);
                                        const aTag = document.createElement('a');
                                        aTag.href = downloadUrl;
                                        aTag.download = filename.replace(/[<>:"/\\|?*]/g, '_') + '.' + downloadSettings.format;
                                        document.body.appendChild(aTag);
                                        aTag.click();
                                        document.body.removeChild(aTag);
                                        URL.revokeObjectURL(downloadUrl);
                                        resolve();
                                    } catch (error) {
                                        reject(error);
                                    }
                                } else {
                                    downloadImage(url, filename).then(resolve).catch(reject);
                                }
                            }, mimeType, quality);
                            
                        } catch (error) {
                            downloadImage(url, filename).then(resolve).catch(reject);
                        }
                    };
                    
                    img.onerror = function() {
                        downloadImage(url, filename).then(resolve).catch(reject);
                    };
                    
                    img.src = URL.createObjectURL(blob);
                });
                
            } catch (error) {
                await downloadImage(url, filename);
            }
        } else {
            // 直接下载
            await downloadImage(url, filename);
        }
    };
    
    window.downloadAll = async function() {
        if (!imageInfos || imageInfos.length === 0) {
            alert('❌ 没有可下载的图片!');
            return;
        }
        
        const total = imageInfos.length;
        let completed = 0;
        
        // 显示下载进度
        const showProgress = () => {
            // 更新面板中的进度显示
            const statusDiv = document.getElementById('downloadStatus');
            const progressSpan = document.getElementById('downloadProgress');
            
            if (statusDiv && progressSpan) {
                statusDiv.style.display = 'block';
                progressSpan.textContent = `下载进度: ${completed}/${total}`;
                
                if (completed === total) {
                    statusDiv.style.background = '#d1ecf1';
                    statusDiv.style.borderColor = '#bee5eb';
                    statusDiv.style.color = '#0c5460';
                    progressSpan.textContent = `✅ 下载完成!共 ${total} 张图片`;
                    
                    // 3秒后隐藏状态
                    setTimeout(() => {
                        statusDiv.style.display = 'none';
                    }, 3000);
                }
            }
        };
        
        // 显示初始状态
        showProgress();
        
        // 顺序下载图片(完全使用1.html方案)
        for (let i = 0; i < imageInfos.length; i++) {
            try {
                const img = imageInfos[i];
                const filename = `${i + 1}`; // 使用数字序号
                
                // 使用1.html的下载方案
                await downloadImage(img.url, filename);
                
                completed++;
                showProgress();
                
                // 延迟避免浏览器限制
                if (i < imageInfos.length - 1) {
                    await new Promise(resolve => setTimeout(resolve, 500));
                }
                
            } catch (error) {
                completed++;
                showProgress();
            }
        }
    };
    
    window.copyUrls = function() {
        const urls = imageInfos.map(img => img.url).join('\n');
        navigator.clipboard.writeText(urls).then(() => {
            alert('图片链接已复制!');
        }).catch(() => {
            const textArea = document.createElement('textarea');
            textArea.value = urls;
            document.body.appendChild(textArea);
            textArea.select();
            document.execCommand('copy');
            document.body.removeChild(textArea);
            alert('图片链接已复制!');
        });
    };
    
    // 删除图片功能
    window.removeImage = function(index) {
        if (imageInfos && imageInfos.length > index) {
            imageInfos.splice(index, 1)[0];
            
            // 更新面板显示
            updatePanel();
        }
    };
    
    
    // 拦截请求 - 简化版(参考test.js)
    const originalXHR = window.XMLHttpRequest;
    window.XMLHttpRequest = function() {
        const xhr = new originalXHR();
        const originalOpen = xhr.open;
        const originalSend = xhr.send;
        
        xhr.open = function(method, url, async, user, password) {
            this._method = method;
            this._url = url;
            return originalOpen.apply(this, arguments);
        };
        
        xhr.send = function(data) {
            this.addEventListener('readystatechange', function() {
                if (this.readyState === 4 && this.status === 200 && this._url.includes('/h5/mtop.taobao.idle.pc.detai')) {
                    try {
                        const jsonData = JSON.parse(this.responseText);
                        if (jsonData?.data?.itemDO?.imageInfos) {
                            itemData = jsonData.data.itemDO;
                            imageInfos = itemData.imageInfos;
                            
                            if (!panel) createMiniPanel();
                            updatePanel();
                        }
                    } catch (e) {
                        // 解析失败,静默处理
                    }
                }
            });
            return originalSend.apply(this, arguments);
        };
        return xhr;
    };
    
   

    // 自动创建面板(不依赖测试按钮)
    function autoCreatePanel() {
        if (document.body && !panel) {
            createMiniPanel();
            
            // 如果5秒后还没有数据,使用测试数据
            setTimeout(() => {
                if (!itemData || !imageInfos.length) {
                    itemData = {
                        title: '等待获取商品数据...',
                        soldPrice: '--',
                        desc: '正在拦截API请求,请稍候...'
                    };
                    imageInfos = [];
                    updatePanel();
                }
            }, 5000);
        }
    }
    
    // 页面加载完成后自动创建面板
    setTimeout(autoCreatePanel, 1000);
    

    
    // Mixed Content检测和修复功能(内部使用)
    function fixMixedContent(url) {
        if (!url) return url;
        
        // 检测Mixed Content问题
        const isHttpsPage = window.location.protocol === 'https:';
        const isHttpUrl = url.startsWith('http://');
        
        if (isHttpsPage && isHttpUrl) {
            const httpsUrl = url.replace('http://', 'https://');
            return httpsUrl;
        }
        
        return url;
    }
    
    // 专业的开发者信息和功能说明
    console.log('%c🛍️ Goofish闲鱼商品图片自动下载器 v1.1', 'color: #0078d4; font-size: 16px; font-weight: bold;');
    console.log('%c━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', 'color: #0078d4;');
    console.log('%c📋 功能说明:', 'color: #28a745; font-weight: bold;');
    console.log('  • 自动拦截Goofish闲鱼商品API数据');
    console.log('  • 显示商品信息(标题、价格、描述)');
    console.log('  • 九宫格图片预览和下载');
    console.log('  • 支持单张/批量下载(数字序号命名)');
    console.log('  • 图片删除功能');
    console.log('  • Win11风格拖拽面板');
    console.log('  • 自动修复Mixed Content问题');
    console.log('  • 作者Email:雷锋[email protected]');
    console.log('%c━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', 'color: #0078d4;');
    console.log('%c✅ 脚本已启动,等待拦截商品数据...', 'color: #28a745; font-weight: bold;');
})();