// ==UserScript== // @name 图片爬虫|图片批量自动打包下载|网页图片批量下载器V6 // @namespace http://tampermonkey.net/ // @version 6.0 // @description 自动爬取网页图片并支持预览下载,多线程并发下载,无限滚动加载 // @author 白虎万岁 // @license MIT // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_openInTab // @grant GM_notification // @grant GM_log // @grant GM_download // @connect * // @run-at document-end // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js // @downloadURL https://update.greasyfork.icu/scripts/571178/%E5%9B%BE%E7%89%87%E7%88%AC%E8%99%AB%7C%E5%9B%BE%E7%89%87%E6%89%B9%E9%87%8F%E8%87%AA%E5%8A%A8%E6%89%93%E5%8C%85%E4%B8%8B%E8%BD%BD%7C%E7%BD%91%E9%A1%B5%E5%9B%BE%E7%89%87%E6%89%B9%E9%87%8F%E4%B8%8B%E8%BD%BD%E5%99%A8V6.user.js // @updateURL https://update.greasyfork.icu/scripts/571178/%E5%9B%BE%E7%89%87%E7%88%AC%E8%99%AB%7C%E5%9B%BE%E7%89%87%E6%89%B9%E9%87%8F%E8%87%AA%E5%8A%A8%E6%89%93%E5%8C%85%E4%B8%8B%E8%BD%BD%7C%E7%BD%91%E9%A1%B5%E5%9B%BE%E7%89%87%E6%89%B9%E9%87%8F%E4%B8%8B%E8%BD%BD%E5%99%A8V6.meta.js // ==/UserScript== (function () { 'use strict'; // ─── 配置 ──────────────────────────────────────────────────────────────── const CONFIG = { CONCURRENT_DOWNLOADS: 6, RETRY_MAX: 3, RETRY_DELAY_BASE: 800, TIMEOUT: 30000, LAZY_BATCH: 20, LAZY_DELAY: 16, MAX_PREVIEW_SIZE: 2000, ITEMS_PER_LOAD: GM_getValue('itemsPerLoad', 20), }; // ─── 全局状态 ───────────────────────────────────────────────────────────── let imageUrls = new Set(); let status, modal, overlay, downloadBtn, previewModal; let progressBar, progressText; let imgObserver; // 懒加载观察器 let lazyCheckScheduled = false; // 滚动节流标志 // 无限加载相关 let displayedCount = 0; let allImages = []; let isLoading = false; let touchStartY = 0; let touchEndY = 0; // ─── 并发控制器 ────────────────────────────────────────────────────────── async function runConcurrent(tasks, concurrency, onProgress) { const results = new Array(tasks.length); let index = 0; let done = 0; async function worker() { while (index < tasks.length) { const i = index++; try { results[i] = { ok: true, value: await tasks[i]() }; } catch (e) { results[i] = { ok: false, error: e }; } done++; if (onProgress) onProgress(done, tasks.length); } } const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, worker); await Promise.all(workers); return results; } // ─── DOM 创建 ───────────────────────────────────────────────────────────── function createElements() { // 状态提示 status = document.createElement('div'); status.style.cssText = ` position:fixed;bottom:80px;right:20px;z-index:2147483647; padding:10px 16px;background:rgba(0,0,0,0.75);color:#fff; border-radius:6px;font-size:14px;display:none; max-width:320px;line-height:1.5; `; progressBar = document.createElement('div'); progressBar.style.cssText = ` height:4px;background:#4CAF50;border-radius:2px; width:0%;transition:width 0.2s;margin-top:6px;display:none; `; progressText = document.createElement('span'); status.appendChild(progressText); status.appendChild(progressBar); // 模态框 modal = document.createElement('div'); modal.className = 'image-downloader-modal'; modal.innerHTML = `
⬇️ 下拉加载更多
`; // 遮罩 overlay = document.createElement('div'); overlay.className = 'modal-overlay'; // 大图预览模态框 previewModal = document.createElement('div'); previewModal.className = 'image-preview-modal'; previewModal.innerHTML = ` 预览
在新标签页打开
`; // 悬浮按钮 downloadBtn = document.createElement('div'); downloadBtn.className = 'image-downloader-btn'; downloadBtn.innerHTML = '📷'; downloadBtn.title = '图片批量下载'; } // ─── 样式 ───────────────────────────────────────────────────────────────── function addStyles() { const style = document.createElement('style'); style.textContent = ` .image-downloader-btn { position:fixed;top:50%;right:0;z-index:2147483647 !important; width:50px;height:50px;border-radius:8px 0 0 8px; background:linear-gradient(145deg,#FF9800,#F57C00) !important; color:#fff !important;cursor:pointer;display:flex !important; align-items:center;justify-content:center; box-shadow:0 4px 12px rgba(255,152,0,0.4) !important; font-size:24px;user-select:none;transition:all 0.3s; transform:translateY(-50%); border:none !important; padding:0 !important; margin:0 !important; opacity:1 !important; visibility:visible !important; } .image-downloader-btn:hover { background:linear-gradient(145deg,#FFA726,#FB8C00) !important; box-shadow:0 6px 16px rgba(255,152,0,0.6) !important; transform:translateY(-50%) translateX(-5px); } .image-downloader-modal { position:fixed;top:50%;left:50%; transform:translate(-50%,-50%); z-index:2147483646;width:70vw;height:70vh; background:#fff;border-radius:16px; box-shadow:0 8px 32px rgba(0,0,0,0.15); display:none;flex-direction:column;overflow:hidden; } .modal-header { padding:16px 24px;background:#fff; border-bottom:1px solid #eee; display:flex;justify-content:space-between;align-items:center; gap:16px;flex-wrap:wrap; } .modal-title { font-size:18px;font-weight:600;color:#1976D2; } .header-controls { display:flex;align-items:center;gap:12px; } .items-per-load-control { display:flex;align-items:center;gap:6px; color:#666;font-size:13px; background:#f5f5f5;padding:6px 12px;border-radius:6px; } .items-per-load-input { width:50px;padding:4px 6px;border:1px solid #ddd; border-radius:4px;font-size:13px;text-align:center; } .items-per-load-input:focus { outline:none;border-color:#2196F3;box-shadow:0 0 4px rgba(33,150,243,0.3); } .modal-close { cursor:pointer;font-size:24px;color:#666;transition:all 0.2s; width:32px;height:32px;display:flex;align-items:center; justify-content:center;border-radius:50%;background:#f5f5f5; } .modal-close:hover { color:#fff;background:#f44336; } .pull-to-refresh { padding:12px;text-align:center;color:#999;font-size:12px; background:#f9f9f9;border-bottom:1px solid #eee; transition:all 0.3s; } .pull-to-refresh.pulling { background:#e3f2fd;color:#1976D2; } .modal-content { flex:1;padding:20px;overflow-y:auto;overflow-x:hidden; display:grid; grid-template-columns:repeat(auto-fill,minmax(160px,1fr)); gap:16px;background:#f5f5f5; touch-action:pan-y; } .modal-content::-webkit-scrollbar { width:8px; } .modal-content::-webkit-scrollbar-thumb { background:#ccc;border-radius:4px; } .image-item { position:relative;padding-top:100%; border:2px solid transparent;border-radius:12px; cursor:pointer;transition:all 0.2s; background:#fff;overflow:hidden; } .image-item::before { content:'';position:absolute;top:10px;right:10px; width:20px;height:20px;border-radius:50%; border:2px solid #fff;background:rgba(0,0,0,0.3); z-index:2; } .image-item.selected::before { background:#2196F3; } .image-item:hover { transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,0.15); } .image-item.selected { border-color:#2196F3; } .image-item img { position:absolute;top:0;left:0;width:100%;height:100%; object-fit:cover;opacity:0;transition:opacity 0.3s; } .image-item img.loaded { opacity:1; } /* 预览按钮 */ .preview-btn { position:absolute;top:10px;left:10px; width:24px;height:24px;border-radius:50%; background:rgba(0,0,0,0.5);color:#fff;border:none; cursor:pointer;z-index:2;opacity:0; display:flex;align-items:center;justify-content:center; font-size:14px;transition:all 0.2s; } .image-item:hover .preview-btn { opacity:1; } .preview-btn:hover { background:rgba(33,150,243,0.8); } .loading-indicator { color:#999;font-size:14px;padding:20px; } .modal-footer { padding:16px 24px;border-top:1px solid #eee; display:flex;justify-content:space-between;align-items:center; background:#fff;flex-wrap:wrap;gap:10px; } .footer-left, .footer-right { display:flex;align-items:center;gap:10px; } .modal-btn { padding:8px 16px;border:none;border-radius:6px;cursor:pointer; background:#2196F3;color:#fff;font-size:14px; transition:all 0.2s; } .modal-btn:hover { background:#1976D2; } .modal-btn:disabled { background:#ccc;cursor:not-allowed; } .path-btn { background:#4CAF50; } .path-btn:hover { background:#388E3C; } .selected-count { color:#666;font-size:14px;background:#f5f5f5; padding:6px 12px;border-radius:6px; } .modal-overlay { position:fixed;inset:0; background:rgba(0,0,0,0.5); z-index:2147483645;display:none; } /* 大图预览模态框 */ .image-preview-modal { position:fixed;top:0;left:0;right:0;bottom:0; background:rgba(0,0,0,0.9);z-index:2147483647; display:none;flex-direction:column;align-items:center;justify-content:center; } .image-preview-modal.active { display:flex; } .preview-image { max-width:90vw;max-height:80vh;object-fit:contain; border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,0.5); } .preview-info { color:#fff;margin-top:16px;font-size:14px;text-align:center; } .preview-info a { color:#64b5f6;text-decoration:none;word-break:break-all; } .preview-info a:hover { text-decoration:underline; } .preview-actions { position:absolute;bottom:20px;display:flex;gap:12px; } .preview-actions .modal-btn { min-width:100px; } .preview-close { position:absolute;top:20px;right:20px; width:40px;height:40px;border-radius:50%; background:rgba(255,255,255,0.2);color:#fff;border:none; cursor:pointer;font-size:24px;display:flex; align-items:center;justify-content:center; } .preview-close:hover { background:rgba(255,255,255,0.3); } @media (max-width: 768px) { .image-downloader-modal { width:90vw;height:90vh; } } `; document.head.appendChild(style); } // ─── 图片采集 ───────────────────────────────────────────────────────────── function collectPageImages() { const images = new Set(); document.querySelectorAll('img').forEach(img => { if (!isValidImage(img)) return; const candidates = [ img.getAttribute('ess-data'), img.dataset.src, img.dataset.original, img.getAttribute('data-original'), img.getAttribute('data-src'), img.getAttribute('data-actualsrc'), img.getAttribute('data-echo'), img.getAttribute('data-lazy'), img.getAttribute('data-url'), img.getAttribute('data-original-src'), ]; candidates.forEach(src => { if (src && isValidImageUrl(src)) images.add(normalizeUrl(src)); }); }); document.querySelectorAll('*').forEach(el => { try { const bg = window.getComputedStyle(el).backgroundImage; if (!bg || bg === 'none') return; const matches = bg.match(/url\(['"]?(.*?)['"]?\)/g) || []; matches.forEach(u => { const clean = u.replace(/url\(['"]?(.*?)['"]?\)/, '$1'); if (isValidImageUrl(clean)) images.add(normalizeUrl(clean)); }); } catch (_) {} }); document.querySelectorAll('picture source').forEach(src => { (src.srcset || '').split(',').forEach(s => { const url = s.trim().split(' ')[0]; if (isValidImageUrl(url)) images.add(normalizeUrl(url)); }); }); return Array.from(images).slice(0, CONFIG.MAX_PREVIEW_SIZE); } function normalizeUrl(url) { if (!url) return url; if (url.startsWith('//')) return location.protocol + url; if (url.startsWith('/')) return location.origin + url; if (url.startsWith('./') || url.startsWith('../')) { try { return new URL(url, location.href).href; } catch (_) {} } return url; } function isValidImage(img) { if (!img) return false; if (img.complete) return img.naturalWidth > 0 || img.naturalHeight > 0; const r = img.getBoundingClientRect(); return r.width > 0 || r.height > 0; } function isValidImageUrl(url) { if (!url || typeof url !== 'string') return false; try { const clean = url.split('?')[0].split('#')[0].toLowerCase(); const ext = (clean.match(/\.([^.]+)$/) || [])[1]; return ['jpg','jpeg','png','gif','webp','bmp','svg','ico','avif'].includes(ext); } catch (_) { return false; } } function getImageExtension(url) { const m = url.split('?')[0].split('#')[0].match(/\.([^.]+)$/); return m ? m[1].toLowerCase() : 'jpg'; } // ─── 无限加载逻辑 ───────────────────────────────────────────────────────── function loadMoreImages() { if (isLoading || displayedCount >= allImages.length) return; isLoading = true; const loadingIndicator = modal.querySelector('.loading-indicator'); loadingIndicator.style.display = 'block'; setTimeout(() => { const content = modal.querySelector('.modal-content'); const end = Math.min(displayedCount + CONFIG.ITEMS_PER_LOAD, allImages.length); const frag = document.createDocumentFragment(); for (let i = displayedCount; i < end; i++) { const url = allImages[i]; const item = document.createElement('div'); item.className = 'image-item'; const previewBtn = document.createElement('button'); previewBtn.className = 'preview-btn'; previewBtn.innerHTML = '🔍'; previewBtn.title = '预览大图'; const img = document.createElement('img'); img.dataset.src = url; img.alt = ''; item.appendChild(previewBtn); item.appendChild(img); item.addEventListener('click', (e) => { if (!e.target.closest('.preview-btn')) { item.classList.toggle('selected'); updateSelectedCount(); } }); frag.appendChild(item); } content.appendChild(frag); displayedCount = end; requestAnimationFrame(() => lazyLoadVisible(content)); loadingIndicator.style.display = 'none'; isLoading = false; // 更新提示文字 const refreshIcon = modal.querySelector('.refresh-icon'); if (displayedCount >= allImages.length) { refreshIcon.textContent = '✅ 已加载全部 ' + allImages.length + ' 张图片'; } else { refreshIcon.textContent = `⬇️ 已加载 ${displayedCount}/${allImages.length},继续下拉加载`; } }, 300); } // ─── 预览 ──────────────────────────────────────────────────────────────── function showPreview() { const content = modal.querySelector('.modal-content'); content.innerHTML = ''; displayedCount = 0; modal.style.display = 'flex'; overlay.style.display = 'block'; updateSelectedCount(); // 初始加载第一批 loadMoreImages(); // 滚动节流 content.onscroll = () => { if (!lazyCheckScheduled) { lazyCheckScheduled = true; requestAnimationFrame(() => { lazyLoadVisible(content); lazyCheckScheduled = false; }); } // 检测滚到底部,自动加载更多 const isAtBottom = content.scrollHeight - content.scrollTop - content.clientHeight < 100; if (isAtBottom && !isLoading && displayedCount < allImages.length) { loadMoreImages(); } }; // 手机下拉加载 let pullStartY = 0; const pullToRefresh = modal.querySelector('.pull-to-refresh'); content.addEventListener('touchstart', (e) => { pullStartY = e.changedTouches[0].clientY; touchStartY = e.changedTouches[0].clientY; }, false); content.addEventListener('touchmove', (e) => { const currentY = e.changedTouches[0].clientY; const diff = currentY - pullStartY; if (content.scrollTop === 0 && diff > 0) { pullToRefresh.classList.add('pulling'); pullToRefresh.textContent = diff > 80 ? '⬆️ 释放加载更多' : '⬇️ 下拉加载更多'; } }, false); content.addEventListener('touchend', (e) => { touchEndY = e.changedTouches[0].clientY; const diff = touchEndY - touchStartY; pullToRefresh.classList.remove('pulling'); // 下拉超过80px触发加载 if (content.scrollTop === 0 && diff > 80 && !isLoading && displayedCount < allImages.length) { loadMoreImages(); } }, false); } // 记录已观察的图片,避免重复 const observedImgs = new WeakSet(); function lazyLoadVisible(container) { if (!imgObserver) return; container.querySelectorAll('img[data-src]:not([src])').forEach(img => { if (!observedImgs.has(img)) { observedImgs.add(img); imgObserver.observe(img); } }); } function updateSelectedCount() { const n = modal.querySelectorAll('.image-item.selected').length; modal.querySelector('.selected-count').textContent = `已选择: ${n}`; modal.querySelector('.download-btn').disabled = n === 0; modal.querySelector('.download-zip-btn').disabled = n === 0; } // ─── 进度显示 ───────────────────────────────────────────────────────────── function showProgress(msg, pct) { progressText.textContent = msg; if (pct !== undefined) { progressBar.style.display = 'block'; progressBar.style.width = pct + '%'; } else { progressBar.style.display = 'none'; } status.style.display = 'block'; } function hideStatus() { status.style.display = 'none'; progressBar.style.display = 'none'; progressBar.style.width = '0%'; } function showStatus(msg, duration = 2000) { progressText.textContent = msg; progressBar.style.display = 'none'; status.style.display = 'block'; setTimeout(hideStatus, duration); } // ─── 下载功能 ───────────────────────────────────────────────────────────── function downloadImage(url, attempt = 0) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url, responseType: 'blob', headers: { 'Referer': location.href, 'User-Agent': navigator.userAgent }, timeout: CONFIG.TIMEOUT, onload(res) { if (res.status === 200) return resolve(res.response); if (attempt < CONFIG.RETRY_MAX) { setTimeout(() => downloadImage(url, attempt + 1).then(resolve, reject), CONFIG.RETRY_DELAY_BASE * (attempt + 1)); } else { reject(new Error(`HTTP ${res.status}`)); } }, onerror(e) { if (attempt < CONFIG.RETRY_MAX) { setTimeout(() => downloadImage(url, attempt + 1).then(resolve, reject), CONFIG.RETRY_DELAY_BASE * (attempt + 1)); } else { reject(new Error('网络错误')); } }, ontimeout() { if (attempt < CONFIG.RETRY_MAX) { setTimeout(() => downloadImage(url, attempt + 1).then(resolve, reject), CONFIG.RETRY_DELAY_BASE * (attempt + 1)); } else { reject(new Error('超时')); } } }); }); } async function downloadIndividual(images) { showProgress(`准备下载 ${images.length} 张图片...`, 0); let failed = 0; const tasks = images.map(img => async () => { const blob = await downloadImage(img.src); const name = img.src.split('/').pop().split('?')[0].split('#')[0]; const base = name.replace(/\.[^.]+$/, '') || `image_${img.index}`; const ext = getImageExtension(img.src); const fileName = `${base}.${ext}`; const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); }); await runConcurrent(tasks, CONFIG.CONCURRENT_DOWNLOADS, (done, total) => { showProgress(`已下载 ${done}/${total} 张...`, Math.round(done / total * 100)); }); closeModal(); showProgress('下载完成!', 100); setTimeout(hideStatus, 3000); } async function downloadZip(images) { showProgress(`准备下载 ${images.length} 张图片...`, 0); const zip = new JSZip(); let failed = 0; const tasks = images.map((img, i) => async () => { const blob = await downloadImage(img.src); const name = img.src.split('/').pop().split('?')[0].split('#')[0]; const base = name.replace(/\.[^.]+$/, '') || `image_${i}`; const ext = getImageExtension(img.src); zip.file(`${base}.${ext}`, blob); }); const results = await runConcurrent(tasks, CONFIG.CONCURRENT_DOWNLOADS, (done, total) => { showProgress(`正在下载 ${done}/${total} 张...`, Math.round(done / total * 80)); }); failed = results.filter(r => !r.ok).length; if (results.every(r => !r.ok)) { showProgress('所有图片下载失败'); setTimeout(hideStatus, 3000); return; } showProgress('正在生成压缩包...', 85); let zipName; try { const el = document.getElementsByClassName('f16')[0]; zipName = el ? el.textContent.trim() : ''; } catch (_) {} if (!zipName) { const pageTitle = document.title.replace(/[\\/:*?"<>|]/g, '_'); const date = new Date().toISOString().split('T')[0]; zipName = `${pageTitle}_${date}`; } const content = await zip.generateAsync( { type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 6 } }, meta => showProgress(`打包中 ${Math.round(meta.percent)}%...`, 85 + meta.percent * 0.15) ); const zipFileName = `${zipName}.zip`; const url = URL.createObjectURL(content); const a = document.createElement('a'); a.href = url; a.download = zipFileName; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); closeModal(); showProgress(failed > 0 ? `完成,${failed} 张失败` : '下载完成!', 100); setTimeout(hideStatus, 3000); } function closeModal() { modal.style.display = 'none'; overlay.style.display = 'none'; } // ─── 事件绑定 ───────────────────────────────────────────────────────────── function setupEventListeners() { downloadBtn.addEventListener('click', () => { allImages = Array.from(imageUrls); showPreview(); }); modal.querySelector('.modal-close').addEventListener('click', closeModal); overlay.addEventListener('click', closeModal); // 每次加载数量设置 const itemsPerLoadInput = modal.querySelector('.items-per-load-input'); itemsPerLoadInput.value = CONFIG.ITEMS_PER_LOAD; itemsPerLoadInput.addEventListener('change', (e) => { const value = parseInt(e.target.value); if (value >= 5 && value <= 100) { CONFIG.ITEMS_PER_LOAD = value; GM_setValue('itemsPerLoad', value); } else { e.target.value = CONFIG.ITEMS_PER_LOAD; } }); // 刷新 modal.querySelector('.refresh-btn').addEventListener('click', () => { imageUrls = new Set(collectPageImages()); allImages = Array.from(imageUrls); displayedCount = 0; showPreview(); }); // 全选 modal.querySelector('.select-all-btn').addEventListener('click', function () { const items = modal.querySelectorAll('.image-item'); const allSelected = Array.from(items).every(i => i.classList.contains('selected')); items.forEach(i => i.classList.toggle('selected', !allSelected)); this.textContent = allSelected ? '全选' : '取消全选'; updateSelectedCount(); }); // 单张下载 modal.querySelector('.download-btn').addEventListener('click', async () => { const selected = Array.from(modal.querySelectorAll('.image-item.selected img')) .map((img, i) => ({ src: img.dataset.src || img.src, index: i })); if (!selected.length) return; await downloadIndividual(selected); }); // 打包下载 modal.querySelector('.download-zip-btn').addEventListener('click', async () => { const selected = Array.from(modal.querySelectorAll('.image-item.selected img')) .map((img, i) => ({ src: img.dataset.src || img.src, index: i })); if (!selected.length) return; await downloadZip(selected); }); // 大图预览功能 const previewImage = previewModal.querySelector('.preview-image'); const previewLink = previewModal.querySelector('.preview-link'); modal.querySelector('.modal-content').addEventListener('click', async (e) => { const previewBtn = e.target.closest('.preview-btn'); if (!previewBtn) return; e.stopPropagation(); const item = previewBtn.closest('.image-item'); const img = item.querySelector('img'); const src = img.dataset.src || img.src; previewImage.src = src; previewLink.href = src; previewModal.classList.add('active'); }); // 关闭预览 previewModal.querySelector('.preview-close').addEventListener('click', () => { previewModal.classList.remove('active'); }); previewModal.querySelector('.preview-close-btn').addEventListener('click', () => { previewModal.classList.remove('active'); }); previewModal.addEventListener('click', (e) => { if (e.target === previewModal) { previewModal.classList.remove('active'); } }); // 预览中下载此图 previewModal.querySelector('.preview-download-btn').addEventListener('click', async () => { const src = previewImage.src; const name = src.split('/').pop().split('?')[0].split('#')[0] || 'image'; const ext = getImageExtension(src); const fileName = `${name.replace(/\.[^.]+$/, '')}.${ext}`; const blob = await downloadImage(src); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); }); } // ─── 初始化 ─────────────────────────────────────────────────────────────── function init() { try { imgObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (!entry.isIntersecting) return; const img = entry.target; const src = img.dataset.src; if (!src) return; img.src = src; img.onload = () => img.classList.add('loaded'); img.onerror = () => {}; imgObserver.unobserve(img); }); }, { rootMargin: '200px' }); createElements(); addStyles(); document.body.appendChild(status); document.body.appendChild(downloadBtn); document.body.appendChild(modal); document.body.appendChild(overlay); document.body.appendChild(previewModal); setupEventListeners(); // 确保按钮可见 setTimeout(() => { downloadBtn.style.display = 'flex'; downloadBtn.style.visibility = 'visible'; downloadBtn.style.opacity = '1'; }, 100); setTimeout(() => { imageUrls = new Set(collectPageImages()); console.log('[图片下载器 V6] 采集到 ' + imageUrls.size + ' 张图片'); }, 500); console.log('[图片下载器 V6] 初始化成功 - 支持无限滚动加载'); } catch (e) { console.error('[图片下载器 V6] 初始化失败:', e); } } if (navigator.userAgent.includes('Edg/')) { window.addEventListener('load', () => setTimeout(init, 500)); } else if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();