// ==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();
}
})();