您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
批量下载页面图片,支持去重、打包ZIP下载,保留原文件名
// ==UserScript== // @name 批量图片下载器 // @namespace http://tampermonkey.net/ // @version 1.2 // @description 批量下载页面图片,支持去重、打包ZIP下载,保留原文件名 // @author upsky // @match *://*/* // @grant none // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js // ==/UserScript== (function() { 'use strict'; let isScriptEnabled = false; let scannedImages = new Set(); let imageList = []; let panel = null; let floatingButton = null; // 从URL中提取文件名 function extractFileName(url) { try { const urlObj = new URL(url); let pathname = urlObj.pathname; // 移除查询参数 pathname = pathname.split('?')[0]; // 获取文件名部分 let filename = pathname.split('/').pop(); // 如果没有文件名或者文件名为空 if (!filename || filename === '') { filename = 'image'; } // 如果没有扩展名,尝试从Content-Type或默认为jpg if (!filename.includes('.')) { filename += '.jpg'; } // 清理文件名中的特殊字符 filename = filename.replace(/[<>:"/\\|?*]/g, '_'); // 限制文件名长度 if (filename.length > 100) { const ext = filename.split('.').pop(); const name = filename.substring(0, 100 - ext.length - 1); filename = name + '.' + ext; } return filename; } catch (error) { console.error('提取文件名失败:', error); return 'image.jpg'; } } // 检查并处理重复文件名 function getUniqueFileName(filename, existingNames) { let uniqueName = filename; let counter = 1; while (existingNames.has(uniqueName)) { const lastDotIndex = filename.lastIndexOf('.'); if (lastDotIndex > 0) { const name = filename.substring(0, lastDotIndex); const ext = filename.substring(lastDotIndex); uniqueName = `${name}_${counter}${ext}`; } else { uniqueName = `${filename}_${counter}`; } counter++; } existingNames.add(uniqueName); return uniqueName; } // 创建浮动按钮 function createFloatingButton() { const button = document.createElement('div'); button.id = 'image-downloader-btn'; button.innerHTML = '📷'; button.style.cssText = ` position: fixed !important; bottom: 50px !important; right: 20px !important; width: 50px !important; height: 50px !important; background: #007bff !important; color: white !important; border: 2px solid #0056b3 !important; border-radius: 50% !important; cursor: pointer !important; font-size: 20px !important; z-index: 999999 !important; box-shadow: 0 4px 15px rgba(0,123,255,0.4) !important; display: flex !important; align-items: center !important; justify-content: center !important; transition: all 0.3s ease !important; user-select: none !important; font-family: Arial, sans-serif !important; `; button.addEventListener('mouseenter', () => { button.style.transform = 'scale(1.1) !important'; button.style.boxShadow = '0 6px 20px rgba(0,123,255,0.6) !important'; }); button.addEventListener('mouseleave', () => { button.style.transform = 'scale(1) !important'; button.style.boxShadow = '0 4px 15px rgba(0,123,255,0.4) !important'; }); button.addEventListener('click', togglePanel); document.body.appendChild(button); return button; } // 创建主界面 function createUI() { const panelContainer = document.createElement('div'); panelContainer.id = 'image-downloader-panel'; panelContainer.style.cssText = ` position: fixed !important; top: 80px !important; right: 20px !important; width: 400px !important; min-height: 70vh !important; background: #ffffff !important; border: 2px solid #007bff !important; border-radius: 12px !important; box-shadow: 0 8px 32px rgba(0,0,0,0.3) !important; z-index: 999998 !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important; display: none !important; overflow: hidden !important; `; panelContainer.innerHTML = ` <div style=" background: linear-gradient(135deg, #007bff, #0056b3) !important; color: white !important; padding: 16px !important; font-weight: bold !important; text-align: center !important; position: relative !important; font-size: 16px !important; "> 🖼️ 批量图片下载器 v1.2 <button id="close-panel" style=" position: absolute !important; right: 12px !important; top: 50% !important; transform: translateY(-50%) !important; background: rgba(255,255,255,0.2) !important; border: none !important; color: white !important; font-size: 20px !important; cursor: pointer !important; width: 28px !important; height: 28px !important; border-radius: 50% !important; display: flex !important; align-items: center !important; justify-content: center !important; ">×</button> </div> <div style="padding: 20px !important; background: white !important;"> <div style="margin-bottom: 16px !important;"> <button id="toggle-script" style=" width: 100% !important; padding: 12px !important; background: #28a745 !important; color: white !important; border: none !important; border-radius: 6px !important; cursor: pointer !important; font-size: 14px !important; font-weight: bold !important; margin-bottom: 12px !important; transition: background 0.3s ease !important; ">🔧 启用脚本</button> <div style="display: flex !important; gap: 8px !important;"> <button id="scan-images" style=" flex: 1 !important; padding: 10px !important; background: #17a2b8 !important; color: white !important; border: none !important; border-radius: 6px !important; cursor: pointer !important; font-size: 13px !important; font-weight: bold !important; " disabled>🔍 扫描图片</button> <button id="clear-images" style=" flex: 1 !important; padding: 10px !important; background: #ffc107 !important; color: #212529 !important; border: none !important; border-radius: 6px !important; cursor: pointer !important; font-size: 13px !important; font-weight: bold !important; " disabled>🗑️ 清空</button> </div> </div> <div style="margin-bottom: 16px !important;"> <div style=" display: flex !important; justify-content: space-between !important; align-items: center !important; margin-bottom: 12px !important; padding: 8px 12px !important; background: #f8f9fa !important; border-radius: 6px !important; "> <span style="font-size: 14px !important; font-weight: bold !important; color: #495057 !important;"> 📊 已扫描: <span id="image-count" style="color: #007bff !important;">0</span> 张 </span> <button id="select-all" style=" padding: 6px 12px !important; background: #6c757d !important; color: white !important; border: none !important; border-radius: 4px !important; cursor: pointer !important; font-size: 12px !important; font-weight: bold !important; " disabled>☑️ 全选</button> </div> <div id="image-list" style=" max-height: 300px !important; overflow-y: auto !important; border: 2px solid #e9ecef !important; border-radius: 8px !important; padding: 8px !important; background: #fafafa !important; "></div> </div> <button id="download-selected" style=" width: 100% !important; padding: 14px !important; background: #dc3545 !important; color: white !important; border: none !important; border-radius: 8px !important; cursor: pointer !important; font-size: 15px !important; font-weight: bold !important; transition: background 0.3s ease !important; " disabled>📦 下载选中图片</button> <div id="progress-container" style=" margin-top: 16px !important; display: none !important; padding: 12px !important; background: #f8f9fa !important; border-radius: 8px !important; border: 1px solid #dee2e6 !important; "> <div style="margin-bottom: 8px !important; font-size: 13px !important; font-weight: bold !important; color: #495057 !important;" id="progress-text">准备下载...</div> <div style=" width: 100% !important; height: 24px !important; background: #e9ecef !important; border-radius: 12px !important; overflow: hidden !important; border: 1px solid #dee2e6 !important; "> <div id="progress-bar" style=" height: 100% !important; background: linear-gradient(90deg, #007bff, #0056b3) !important; width: 0% !important; transition: width 0.3s ease !important; border-radius: 12px !important; "></div> </div> </div> </div> `; document.body.appendChild(panelContainer); return panelContainer; } // 切换面板显示 function togglePanel() { if (!panel) return; const isVisible = panel.style.display !== 'none'; panel.style.display = isVisible ? 'none' : 'block'; if (!isVisible) { panel.style.zIndex = '999998'; } } // 扫描页面图片 function scanImages() { const images = document.querySelectorAll('img'); let newImagesCount = 0; images.forEach(img => { if (img.src && img.src.startsWith('http') && !scannedImages.has(img.src)) { // 检查图片尺寸,过滤掉太小的图片 if (img.naturalWidth > 50 && img.naturalHeight > 50) { scannedImages.add(img.src); // 提取原始文件名 const originalFileName = extractFileName(img.src); imageList.push({ src: img.src, alt: img.alt || originalFileName, fileName: originalFileName, selected: false }); newImagesCount++; } } }); updateImageList(); showNotification(newImagesCount > 0 ? `🎉 发现 ${newImagesCount} 张新图片` : '😅 未发现新图片'); } // 更新图片列表显示 function updateImageList() { const imageListElement = document.getElementById('image-list'); const imageCountElement = document.getElementById('image-count'); if (!imageListElement || !imageCountElement) return; imageCountElement.textContent = imageList.length; imageListElement.innerHTML = ''; if (imageList.length === 0) { imageListElement.innerHTML = ` <div style=" text-align: center !important; padding: 40px 20px !important; color: #6c757d !important; font-size: 14px !important; "> 📷 暂无图片<br> <small style="color: #adb5bd !important;">点击"扫描图片"开始搜索</small> </div> `; } else { imageList.forEach((image, index) => { const imageItem = document.createElement('div'); imageItem.style.cssText = ` display: flex !important; align-items: center !important; padding: 10px !important; margin-bottom: 8px !important; border: 1px solid ${image.selected ? '#007bff' : '#e9ecef'} !important; border-radius: 8px !important; background: ${image.selected ? '#e3f2fd' : '#ffffff'} !important; transition: all 0.2s ease !important; cursor: pointer !important; `; imageItem.innerHTML = ` <input type="checkbox" ${image.selected ? 'checked' : ''} style=" margin-right: 12px !important; transform: scale(1.2) !important; cursor: pointer !important; "> <img src="${image.src}" style=" width: 50px !important; height: 50px !important; object-fit: cover !important; border-radius: 6px !important; margin-right: 12px !important; border: 2px solid #e9ecef !important; " onerror="this.style.display='none'"> <div style="flex: 1 !important; font-size: 12px !important; overflow: hidden !important;"> <div style="font-weight: bold !important; margin-bottom: 4px !important; color: #495057 !important;"> 📄 ${image.fileName} </div> <div style="color: #28a745 !important; font-size: 11px !important; margin-bottom: 2px !important;"> 原文件名: ${image.fileName} </div> <div style="color: #6c757d !important; word-break: break-all !important; font-size: 10px !important;"> ${image.src.length > 50 ? image.src.substring(0, 50) + '...' : image.src} </div> </div> `; // 点击整个项目来切换选择状态 imageItem.addEventListener('click', () => { imageList[index].selected = !imageList[index].selected; updateImageList(); }); imageListElement.appendChild(imageItem); }); } updateButtonStates(); } // 更新按钮状态 function updateButtonStates() { const hasImages = imageList.length > 0; const hasSelected = imageList.some(img => img.selected); const allSelected = imageList.length > 0 && imageList.every(img => img.selected); const clearBtn = document.getElementById('clear-images'); const selectAllBtn = document.getElementById('select-all'); const downloadBtn = document.getElementById('download-selected'); if (clearBtn) clearBtn.disabled = !hasImages; if (selectAllBtn) { selectAllBtn.disabled = !hasImages; selectAllBtn.textContent = allSelected ? '❌ 取消全选' : '☑️ 全选'; } if (downloadBtn) downloadBtn.disabled = !hasSelected; } // 全选/取消全选 function toggleSelectAll() { const allSelected = imageList.every(img => img.selected); imageList.forEach(img => { img.selected = !allSelected; }); updateImageList(); } // 下载选中的图片 async function downloadSelectedImages() { const selectedImages = imageList.filter(img => img.selected); if (selectedImages.length === 0) { showNotification('⚠️ 请先选择要下载的图片'); return; } const progressContainer = document.getElementById('progress-container'); const progressBar = document.getElementById('progress-bar'); const progressText = document.getElementById('progress-text'); if (!progressContainer || !progressBar || !progressText) return; progressContainer.style.display = 'block'; progressText.textContent = '🚀 准备下载...'; progressBar.style.width = '0%'; const zip = new JSZip(); const usedFileNames = new Set(); let completed = 0; try { for (let i = 0; i < selectedImages.length; i++) { const image = selectedImages[i]; progressText.textContent = `📥 下载图片 ${i + 1}/${selectedImages.length} - ${image.fileName}`; try { const response = await fetch(image.src); const blob = await response.blob(); // 使用原始文件名,如果重复则添加序号 const uniqueFileName = getUniqueFileName(image.fileName, usedFileNames); zip.file(uniqueFileName, blob); completed++; const progress = (completed / selectedImages.length) * 80; progressBar.style.width = progress + '%'; } catch (error) { console.error('下载图片失败:', image.src, error); // 即使下载失败也要添加文件名到已使用列表,避免序号混乱 getUniqueFileName(image.fileName, usedFileNames); } } progressText.textContent = '📦 正在打包ZIP文件...'; progressBar.style.width = '90%'; const zipBlob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 6 } }); progressText.textContent = '✅ 下载完成!'; progressBar.style.width = '100%'; // 创建下载链接 const link = document.createElement('a'); link.href = URL.createObjectURL(zipBlob); link.download = `images_${new Date().toISOString().slice(0,19).replace(/:/g, '-')}.zip`; link.click(); setTimeout(() => { progressContainer.style.display = 'none'; URL.revokeObjectURL(link.href); }, 3000); showNotification(`🎉 成功下载 ${completed} 张图片,保留原文件名`); } catch (error) { console.error('打包失败:', error); showNotification('❌ 下载失败,请重试'); progressContainer.style.display = 'none'; } } // 显示通知 function showNotification(message) { const notification = document.createElement('div'); notification.style.cssText = ` position: fixed !important; top: 80px !important; right: 430px !important; background: #333 !important; color: white !important; padding: 12px 16px !important; border-radius: 8px !important; z-index: 999999 !important; font-size: 14px !important; font-weight: bold !important; max-width: 300px !important; box-shadow: 0 4px 12px rgba(0,0,0,0.3) !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important; `; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification); } }, 4000); } // 绑定事件 function bindEvents() { // 关闭面板 const closeBtn = document.getElementById('close-panel'); if (closeBtn) { closeBtn.addEventListener('click', () => { panel.style.display = 'none'; }); } // 启用/禁用脚本 const toggleBtn = document.getElementById('toggle-script'); if (toggleBtn) { toggleBtn.addEventListener('click', () => { isScriptEnabled = !isScriptEnabled; const scanButton = document.getElementById('scan-images'); if (isScriptEnabled) { toggleBtn.textContent = '🔧 禁用脚本'; toggleBtn.style.background = '#dc3545'; if (scanButton) scanButton.disabled = false; showNotification('✅ 脚本已启用'); } else { toggleBtn.textContent = '🔧 启用脚本'; toggleBtn.style.background = '#28a745'; if (scanButton) scanButton.disabled = true; showNotification('⏸️ 脚本已禁用'); } }); } // 扫描图片 const scanBtn = document.getElementById('scan-images'); if (scanBtn) { scanBtn.addEventListener('click', scanImages); } // 清空列表 const clearBtn = document.getElementById('clear-images'); if (clearBtn) { clearBtn.addEventListener('click', () => { imageList = []; scannedImages.clear(); updateImageList(); showNotification('🗑️ 已清空图片列表'); }); } // 全选 const selectAllBtn = document.getElementById('select-all'); if (selectAllBtn) { selectAllBtn.addEventListener('click', toggleSelectAll); } // 下载选中图片 const downloadBtn = document.getElementById('download-selected'); if (downloadBtn) { downloadBtn.addEventListener('click', downloadSelectedImages); } } // 初始化 function init() { setTimeout(() => { floatingButton = createFloatingButton(); panel = createUI(); bindEvents(); console.log('🎉 批量图片下载器 v1.2 已加载'); setTimeout(() => { showNotification('🎉 图片下载器已就绪!支持保留原文件名'); }, 1000); }, 1000); } // 确保在页面加载完成后初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();