// ==UserScript==
// @name 网页图片下载
// @version v2.1
// @description 生成一个批量下载网页图片的按钮,支持选择性下载。目前支持网站:1. 微信公众号网页 2. 微博网页 3.其他网页使用通用方法下载
// @author nixingshiguang
// @match http*://mp.weixin.qq.com/s*
// @match https://m.weibo.cn/*
// @match https://weibo.com/*
// @match https://www.threads.com/*
// @icon https://cftc.160621.xyz/file/8d23583061c79384c94e0.png
// @grant GM_download
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @namespace https://greasyfork.org/users/943170
// ==/UserScript==
(function () {
'use strict';
/**
* 图片下载工具类
*/
class ImageDownloader {
constructor() {
this.imgElements = null;
this.formattedDate = this.getFormattedDate();
this.downloadButton = null;
this.selectButton = null;
this.modal = null;
this.selectedImages = new Set();
this.imageData = [];
this.init();
}
/**
* 初始化
*/
init() {
this.createButtons();
this.createModal();
this.setupDownloadHandler();
}
/**
* 获取格式化的日期字符串
* @returns {string} 格式化的日期 "yyyy-mm-dd"
*/
getFormattedDate() {
const today = new Date();
const year = today.getFullYear();
const month = ('0' + (today.getMonth() + 1)).slice(-2);
const day = ('0' + today.getDate()).slice(-2);
return `${year}-${month}-${day}`;
}
/**
* 生成随机字符串
* @param {number} length 字符串长度
* @returns {string} 随机字符串
*/
generateRandomString(length = 4) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
let result = '';
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
}
/**
* 创建按钮
*/
createButtons() {
// 创建按钮容器
const buttonContainer = document.createElement('div');
Object.assign(buttonContainer.style, {
position: 'fixed',
top: '100px',
right: '10px',
zIndex: '9999',
display: 'flex',
flexDirection: 'column',
gap: '10px'
});
// 创建直接下载按钮
this.downloadButton = document.createElement('button');
Object.assign(this.downloadButton.style, {
borderRadius: '10px',
padding: '10px',
border: '0',
color: 'black',
backgroundColor: 'white',
boxShadow: '0 0 5px 5px skyblue',
opacity: '0.8',
cursor: 'pointer',
fontSize: '14px'
});
this.downloadButton.textContent = '直接下载';
// 创建选择下载按钮
this.selectButton = document.createElement('button');
Object.assign(this.selectButton.style, {
borderRadius: '10px',
padding: '10px',
border: '0',
color: 'white',
backgroundColor: '#007bff',
boxShadow: '0 0 5px 5px rgba(0,123,255,0.3)',
opacity: '0.8',
cursor: 'pointer',
fontSize: '14px'
});
this.selectButton.textContent = '选择下载';
buttonContainer.appendChild(this.downloadButton);
buttonContainer.appendChild(this.selectButton);
document.body.appendChild(buttonContainer);
// 绑定选择下载按钮事件
this.selectButton.addEventListener('click', () => {
this.showImageSelector();
});
console.log('添加下载按钮完成');
}
/**
* 创建模态弹窗
*/
createModal() {
this.modal = document.createElement('div');
this.modal.style.cssText = `
display: none;
position: fixed;
z-index: 10000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
`;
const modalContent = document.createElement('div');
modalContent.style.cssText = `
background-color: white;
margin: 2% auto;
padding: 20px;
border-radius: 10px;
width: 90%;
max-width: 800px;
max-height: 90%;
overflow-y: auto;
position: relative;
`;
// 模态框标题
const title = document.createElement('h2');
title.textContent = '选择要下载的图片';
title.style.cssText = `
margin: 0 0 20px 0;
color: #333;
text-align: center;
`;
// 关闭按钮
const closeBtn = document.createElement('span');
closeBtn.innerHTML = '×';
closeBtn.style.cssText = `
position: absolute;
right: 15px;
top: 15px;
font-size: 28px;
font-weight: bold;
cursor: pointer;
color: #aaa;
`;
closeBtn.onclick = () => this.hideModal();
// 控制按钮区域
const controlArea = document.createElement('div');
controlArea.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 10px;
`;
// 全选按钮
const selectAllBtn = document.createElement('button');
selectAllBtn.textContent = '全选';
selectAllBtn.style.cssText = `
padding: 8px 16px;
background: #28a745;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
`;
selectAllBtn.onclick = () => this.selectAll();
// 反选按钮
const invertBtn = document.createElement('button');
invertBtn.textContent = '反选';
invertBtn.style.cssText = `
padding: 8px 16px;
background: #ffc107;
color: black;
border: none;
border-radius: 5px;
cursor: pointer;
`;
invertBtn.onclick = () => this.invertSelection();
// 选中计数
const countLabel = document.createElement('span');
countLabel.id = 'selectedCount';
countLabel.style.cssText = `
font-weight: bold;
color: #007bff;
`;
controlArea.appendChild(selectAllBtn);
controlArea.appendChild(invertBtn);
controlArea.appendChild(countLabel);
// 图片网格容器
const imageGrid = document.createElement('div');
imageGrid.id = 'imageGrid';
imageGrid.style.cssText = `
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 20px;
`;
// 下载按钮区域
const downloadArea = document.createElement('div');
downloadArea.style.cssText = `
text-align: center;
border-top: 1px solid #eee;
padding-top: 20px;
`;
const downloadSelectedBtn = document.createElement('button');
downloadSelectedBtn.textContent = '下载选中图片';
downloadSelectedBtn.style.cssText = `
padding: 12px 24px;
background: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin-right: 10px;
`;
downloadSelectedBtn.onclick = () => this.downloadSelected();
const downloadZipBtn = document.createElement('button');
downloadZipBtn.textContent = '打包下载ZIP';
downloadZipBtn.style.cssText = `
padding: 12px 24px;
background: #6c757d;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
`;
downloadZipBtn.onclick = () => this.downloadAsZip();
// 进度条
const progressContainer = document.createElement('div');
progressContainer.id = 'progressContainer';
progressContainer.style.cssText = `
display: none;
margin-top: 20px;
`;
const progressBar = document.createElement('div');
progressBar.style.cssText = `
width: 100%;
height: 20px;
background-color: #f0f0f0;
border-radius: 10px;
overflow: hidden;
`;
const progressFill = document.createElement('div');
progressFill.id = 'progressFill';
progressFill.style.cssText = `
height: 100%;
background-color: #007bff;
width: 0%;
transition: width 0.3s ease;
`;
const progressText = document.createElement('div');
progressText.id = 'progressText';
progressText.style.cssText = `
text-align: center;
margin-top: 10px;
font-weight: bold;
`;
progressBar.appendChild(progressFill);
progressContainer.appendChild(progressBar);
progressContainer.appendChild(progressText);
downloadArea.appendChild(downloadSelectedBtn);
downloadArea.appendChild(downloadZipBtn);
downloadArea.appendChild(progressContainer);
modalContent.appendChild(closeBtn);
modalContent.appendChild(title);
modalContent.appendChild(controlArea);
modalContent.appendChild(imageGrid);
modalContent.appendChild(downloadArea);
this.modal.appendChild(modalContent);
document.body.appendChild(this.modal);
}
/**
* 通过XMLHttpRequest下载单个图片
* @param {string} url 图片URL
* @param {string} filename 文件名
*/
downloadImageByXHR(url, filename = null) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'blob';
xhr.onload = () => {
if (xhr.status === 200) {
const blob = xhr.response;
const link = document.createElement('a');
link.style.display = 'none';
document.body.appendChild(link);
const blobUrl = window.URL.createObjectURL(blob);
link.href = blobUrl;
link.download = filename || `${this.formattedDate}_${this.generateRandomString()}.jpg`;
link.click();
window.URL.revokeObjectURL(blobUrl);
document.body.removeChild(link);
}
};
xhr.onerror = () => {
console.error(`下载失败: ${url}`);
};
xhr.send();
}
/**
* 通过GM_download下载图片
* @param {string} url 图片URL
* @param {string} filename 文件名
*/
downloadImageByGM(url, filename = null) {
GM_download({
url: url,
name: filename || `${this.formattedDate}_${this.generateRandomString()}.jpg`
});
}
/**
* 批量下载图片(通用方法)
* @param {NodeList} imgElements 图片元素列表
* @param {Function} urlProcessor URL处理函数
* @param {boolean} useXHR 是否使用XHR下载
*/
batchDownload(imgElements, urlProcessor = null, useXHR = false) {
console.group("图片下载脚本");
const imageUrls = [];
let count = 1;
imgElements.forEach(img => {
let url = img.getAttribute('data-src') || img.getAttribute('src');
if (urlProcessor && typeof urlProcessor === 'function') {
url = urlProcessor(url);
}
if (url) {
console.log(`获取第${count}张图片链接: ${url}`);
imageUrls.push(url);
count++;
}
});
console.log("开始下载,共", imageUrls.length, "张图片");
imageUrls.forEach((url, index) => {
const filename = `${this.formattedDate}_${this.generateRandomString()}.jpg`;
if (useXHR) {
this.downloadImageByXHR(url, filename);
} else {
this.downloadImageByGM(url, filename);
}
});
console.log("下载任务已启动");
console.groupEnd();
}
/**
* 显示图片选择器
*/
async showImageSelector() {
// 显示加载提示
this.showLoadingModal();
try {
// 先自动滚动页面加载懒加载图片
await this.autoScrollAndLoadImages();
// 获取图片数据
await this.collectImageData();
if (this.imageData.length === 0) {
this.hideLoadingModal();
alert('未找到可下载的图片');
return;
}
// 隐藏加载提示,显示选择界面
this.hideLoadingModal();
// 渲染图片网格
this.renderImageGrid();
// 显示模态框
this.modal.style.display = 'block';
// 更新选中计数
this.updateSelectedCount();
} catch (error) {
this.hideLoadingModal();
console.error('加载图片时出错:', error);
alert('加载图片时出错,请重试');
}
}
/**
* 显示加载模态框
*/
showLoadingModal() {
// 创建加载遮罩
this.loadingModal = document.createElement('div');
this.loadingModal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 10001;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
`;
const loadingContent = document.createElement('div');
loadingContent.style.cssText = `
background: white;
padding: 30px;
border-radius: 10px;
text-align: center;
max-width: 400px;
`;
const spinner = document.createElement('div');
spinner.style.cssText = `
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px auto;
`;
// 添加旋转动画
const style = document.createElement('style');
style.textContent = `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
const loadingText = document.createElement('div');
loadingText.id = 'loadingText';
loadingText.style.cssText = `
font-size: 16px;
color: #333;
margin-bottom: 10px;
`;
loadingText.textContent = '正在加载页面图片...';
const progressText = document.createElement('div');
progressText.id = 'loadingProgress';
progressText.style.cssText = `
font-size: 14px;
color: #666;
`;
progressText.textContent = '请稍候,正在滚动页面加载所有图片';
loadingContent.appendChild(spinner);
loadingContent.appendChild(loadingText);
loadingContent.appendChild(progressText);
this.loadingModal.appendChild(loadingContent);
document.body.appendChild(this.loadingModal);
}
/**
* 隐藏加载模态框
*/
hideLoadingModal() {
if (this.loadingModal) {
document.body.removeChild(this.loadingModal);
this.loadingModal = null;
}
}
/**
* 自动滚动页面并加载懒加载图片
*/
async autoScrollAndLoadImages() {
const updateProgress = (message) => {
const progressElement = document.getElementById('loadingProgress');
if (progressElement) {
progressElement.textContent = message;
}
};
// 保存当前滚动位置
const originalScrollTop = window.pageYOffset || document.documentElement.scrollTop;
updateProgress('正在扫描页面高度...');
// 获取页面总高度
const getPageHeight = () => Math.max(
document.body.scrollHeight,
document.body.offsetHeight,
document.documentElement.clientHeight,
document.documentElement.scrollHeight,
document.documentElement.offsetHeight
);
let lastHeight = getPageHeight();
let scrollPosition = 0;
const scrollStep = window.innerHeight * 0.8; // 每次滚动80%的视窗高度
let noNewContentCount = 0;
const maxNoNewContentCount = 3; // 连续3次没有新内容就停止
updateProgress('开始自动滚动加载图片...');
while (scrollPosition < lastHeight && noNewContentCount < maxNoNewContentCount) {
// 滚动到指定位置
window.scrollTo(0, scrollPosition);
updateProgress(`正在滚动加载... (${Math.round((scrollPosition / lastHeight) * 100)}%)`);
// 等待图片加载
await new Promise(resolve => setTimeout(resolve, 800));
// 触发懒加载
await this.triggerLazyLoad();
// 再等待一下让图片完全加载
await new Promise(resolve => setTimeout(resolve, 500));
// 检查页面高度是否有变化
const newHeight = getPageHeight();
if (newHeight > lastHeight) {
lastHeight = newHeight;
noNewContentCount = 0; // 重置计数器
} else {
noNewContentCount++;
}
scrollPosition += scrollStep;
}
updateProgress('滚动完成,正在收集图片信息...');
// 最后滚动到底部确保所有内容都加载了
window.scrollTo(0, lastHeight);
await new Promise(resolve => setTimeout(resolve, 1000));
// 再次触发懒加载
await this.triggerLazyLoad();
await new Promise(resolve => setTimeout(resolve, 500));
updateProgress('图片加载完成,正在生成选择界面...');
// 恢复原始滚动位置
window.scrollTo(0, originalScrollTop);
}
/**
* 触发懒加载
*/
async triggerLazyLoad() {
// 触发滚动事件
window.dispatchEvent(new Event('scroll'));
window.dispatchEvent(new Event('resize'));
// 查找所有可能的懒加载图片并尝试加载
const lazyImages = document.querySelectorAll('img[data-src], img[data-original], img[loading="lazy"]');
lazyImages.forEach(img => {
// 模拟图片进入视窗
if (img.dataset.src && !img.src) {
img.src = img.dataset.src;
}
if (img.dataset.original && !img.src) {
img.src = img.dataset.original;
}
// 触发 Intersection Observer (如果页面使用了的话)
const rect = img.getBoundingClientRect();
if (rect.top < window.innerHeight && rect.bottom > 0) {
// 图片在视窗内,触发加载
img.dispatchEvent(new Event('load'));
}
});
// 查找并点击可能的"加载更多"按钮
this.findAndClickLoadMoreButtons();
}
/**
* 查找并点击"加载更多"按钮
*/
findAndClickLoadMoreButtons() {
// 通过类名查找
const classSelectorButtons = document.querySelectorAll('.load-more, .show-more, .btn-load-more, .more-btn, .load-btn');
// 通过文本内容查找按钮和链接
const allButtons = document.querySelectorAll('button, a');
const textBasedButtons = Array.from(allButtons).filter(btn => {
const text = btn.textContent.trim().toLowerCase();
return text.includes('加载更多') ||
text.includes('查看更多') ||
text.includes('显示更多') ||
text.includes('load more') ||
text.includes('show more') ||
text.includes('更多');
});
// 合并所有找到的按钮
const allLoadMoreButtons = [...classSelectorButtons, ...textBasedButtons];
// 去重并点击可见的按钮
const uniqueButtons = [...new Set(allLoadMoreButtons)];
uniqueButtons.forEach(btn => {
if (btn.offsetParent !== null && !btn.disabled) { // 确保按钮可见且未禁用
try {
btn.click();
console.log('点击了加载更多按钮:', btn.textContent.trim());
} catch (error) {
console.warn('点击按钮时出错:', error);
}
}
});
}
/**
* 收集图片数据
*/
async collectImageData() {
const currentUrl = window.location.href;
this.imageData = [];
let imgElements;
let urlProcessor = null;
// 根据网站类型获取图片元素
switch (true) {
case currentUrl.includes("mp.weixin.qq.com/s"):
const articleElement = document.querySelector("#js_article");
if (articleElement) {
if (articleElement.classList.contains('share_content_page')) {
imgElements = document.querySelectorAll('.swiper_item img');
} else {
imgElements = document.querySelectorAll('#img-content img');
}
} else {
imgElements = document.querySelectorAll('img');
}
break;
case currentUrl.includes("https://weibo.com/"):
imgElements = document.querySelectorAll(".Viewer_prevItem_McSJ4 img");
urlProcessor = (url) => url.replace("orj360", "large");
break;
case currentUrl.includes("https://m.weibo.cn/"):
imgElements = document.querySelectorAll("img[data-v-5deaae85]");
urlProcessor = (url) => url.replace("orj360", "large");
break;
case currentUrl.includes("https://www.threads.com/"):
const elements = document.querySelectorAll('picture.x87ps6o');
this.imageData = [];
Array.from(elements).forEach((element, index) => {
try {
if (element.firstChild && element.firstChild.srcset) {
const url = element.firstChild.srcset.split(' ')[0];
if (url && (url.startsWith('http://') || url.startsWith('https://'))) {
this.imageData.push({
id: index,
url: url,
thumbnail: url
});
}
}
} catch (error) {
console.error('处理Threads图片时出错:', error);
}
});
return;
default:
imgElements = document.querySelectorAll('img');
}
// 处理图片元素
if (imgElements) {
Array.from(imgElements).forEach((img, index) => {
let url = img.getAttribute('data-src') || img.getAttribute('src');
if (urlProcessor && typeof urlProcessor === 'function') {
url = urlProcessor(url);
}
if (url && url.startsWith('http')) {
this.imageData.push({
id: index,
url: url,
thumbnail: img.src || url
});
}
});
}
}
/**
* 渲染图片网格
*/
async renderImageGrid() {
const imageGrid = document.getElementById('imageGrid');
imageGrid.innerHTML = '';
// 显示加载状态
const loadingDiv = document.createElement('div');
loadingDiv.textContent = '正在生成图片预览...';
loadingDiv.style.cssText = `
text-align: center;
padding: 20px;
color: #666;
font-size: 16px;
`;
imageGrid.appendChild(loadingDiv);
// 预加载所有图片的缩略图
await this.preloadThumbnails();
// 清除加载状态
imageGrid.innerHTML = '';
this.imageData.forEach((imageInfo) => {
const imageItem = document.createElement('div');
imageItem.style.cssText = `
border: 2px solid #ddd;
border-radius: 8px;
padding: 10px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
`;
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = `img_${imageInfo.id}`;
checkbox.style.cssText = `
margin-bottom: 10px;
transform: scale(1.2);
`;
checkbox.onchange = () => this.toggleImageSelection(imageInfo.id);
const imgContainer = document.createElement('div');
imgContainer.style.cssText = `
position: relative;
width: 100%;
height: 120px;
margin-bottom: 10px;
background: #f5f5f5;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
`;
const img = document.createElement('img');
img.style.cssText = `
max-width: 100%;
max-height: 100%;
object-fit: cover;
border-radius: 5px;
`;
// 使用预加载的缩略图或原图
const thumbnailUrl = imageInfo.preloadedThumbnail || imageInfo.thumbnail || imageInfo.url;
if (thumbnailUrl) {
img.src = thumbnailUrl;
} else {
// 显示占位符
img.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIwIiBoZWlnaHQ9IjEyMCIgdmlld0JveD0iMCAwIDEyMCAxMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxMjAiIGhlaWdodD0iMTIwIiBmaWxsPSIjRjBGMEYwIi8+CjxwYXRoIGQ9Ik02MCA0MEw4MCA3MEg0MEw2MCA0MFoiIGZpbGw9IiNDQ0MiLz4KPC9zdmc+';
}
img.onerror = () => {
// 如果缩略图加载失败,尝试加载原图
if (img.src !== imageInfo.url && imageInfo.url) {
img.src = imageInfo.url;
} else {
// 显示占位符
img.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIwIiBoZWlnaHQ9IjEyMCIgdmlld0JveD0iMCAwIDEyMCAxMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxMjAiIGhlaWdodD0iMTIwIiBmaWxsPSIjRjBGMEYwIi8+CjxwYXRoIGQ9Ik02MCA0MEw4MCA3MEg0MEw2MCA0MFoiIGZpbGw9IiNDQ0MiLz4KPC9zdmc+';
}
};
imgContainer.appendChild(img);
const label = document.createElement('label');
label.htmlFor = `img_${imageInfo.id}`;
label.textContent = `图片 ${imageInfo.id + 1}`;
label.style.cssText = `
display: block;
font-size: 12px;
color: #666;
cursor: pointer;
`;
// 添加图片尺寸信息
const sizeInfo = document.createElement('div');
sizeInfo.style.cssText = `
font-size: 10px;
color: #999;
margin-top: 2px;
`;
// 尝试获取图片尺寸信息
if (imageInfo.dimensions) {
sizeInfo.textContent = `${imageInfo.dimensions.width}×${imageInfo.dimensions.height}`;
} else {
sizeInfo.textContent = '获取尺寸中...';
this.getImageDimensions(imageInfo.url).then(dimensions => {
if (dimensions) {
sizeInfo.textContent = `${dimensions.width}×${dimensions.height}`;
imageInfo.dimensions = dimensions;
} else {
sizeInfo.textContent = '未知尺寸';
}
});
}
imageItem.appendChild(checkbox);
imageItem.appendChild(imgContainer);
imageItem.appendChild(label);
imageItem.appendChild(sizeInfo);
// 点击整个项目来切换选择
imageItem.onclick = (e) => {
if (e.target !== checkbox) {
checkbox.checked = !checkbox.checked;
this.toggleImageSelection(imageInfo.id);
}
};
imageGrid.appendChild(imageItem);
});
}
/**
* 预加载缩略图
*/
async preloadThumbnails() {
const updateProgress = (current, total) => {
const progressElement = document.getElementById('loadingProgress');
if (progressElement) {
progressElement.textContent = `正在预加载图片缩略图... (${current}/${total})`;
}
};
const promises = this.imageData.map(async (imageInfo, index) => {
try {
updateProgress(index + 1, this.imageData.length);
// 尝试创建缩略图
const thumbnailUrl = await this.createThumbnail(imageInfo.url);
if (thumbnailUrl) {
imageInfo.preloadedThumbnail = thumbnailUrl;
}
} catch (error) {
console.warn(`预加载图片 ${imageInfo.url} 失败:`, error);
}
});
await Promise.all(promises);
}
/**
* 创建图片缩略图
*/
async createThumbnail(imageUrl, maxWidth = 200, maxHeight = 200) {
return new Promise((resolve) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
try {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 计算缩略图尺寸
let { width, height } = img;
const ratio = Math.min(maxWidth / width, maxHeight / height);
if (ratio < 1) {
width *= ratio;
height *= ratio;
}
canvas.width = width;
canvas.height = height;
// 绘制缩略图
ctx.drawImage(img, 0, 0, width, height);
// 转换为 data URL
const thumbnailUrl = canvas.toDataURL('image/jpeg', 0.8);
resolve(thumbnailUrl);
} catch (error) {
console.warn('创建缩略图失败:', error);
resolve(imageUrl); // 返回原图URL
}
};
img.onerror = () => {
resolve(imageUrl); // 返回原图URL
};
img.src = imageUrl;
// 设置超时
setTimeout(() => {
resolve(imageUrl);
}, 5000);
});
}
/**
* 获取图片尺寸
*/
async getImageDimensions(imageUrl) {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
resolve({ width: img.naturalWidth, height: img.naturalHeight });
};
img.onerror = () => {
resolve(null);
};
img.src = imageUrl;
// 设置超时
setTimeout(() => {
resolve(null);
}, 3000);
});
}
/**
* 切换图片选择状态
*/
toggleImageSelection(imageId) {
const checkbox = document.getElementById(`img_${imageId}`);
const imageItem = checkbox.parentElement;
if (checkbox.checked) {
this.selectedImages.add(imageId);
imageItem.style.borderColor = '#007bff';
imageItem.style.backgroundColor = '#f8f9fa';
} else {
this.selectedImages.delete(imageId);
imageItem.style.borderColor = '#ddd';
imageItem.style.backgroundColor = 'white';
}
this.updateSelectedCount();
}
/**
* 全选
*/
selectAll() {
this.selectedImages.clear();
this.imageData.forEach(imageInfo => {
this.selectedImages.add(imageInfo.id);
const checkbox = document.getElementById(`img_${imageInfo.id}`);
const imageItem = checkbox.parentElement;
checkbox.checked = true;
imageItem.style.borderColor = '#007bff';
imageItem.style.backgroundColor = '#f8f9fa';
});
this.updateSelectedCount();
}
/**
* 反选
*/
invertSelection() {
const newSelection = new Set();
this.imageData.forEach(imageInfo => {
const checkbox = document.getElementById(`img_${imageInfo.id}`);
const imageItem = checkbox.parentElement;
if (!this.selectedImages.has(imageInfo.id)) {
newSelection.add(imageInfo.id);
checkbox.checked = true;
imageItem.style.borderColor = '#007bff';
imageItem.style.backgroundColor = '#f8f9fa';
} else {
checkbox.checked = false;
imageItem.style.borderColor = '#ddd';
imageItem.style.backgroundColor = 'white';
}
});
this.selectedImages = newSelection;
this.updateSelectedCount();
}
/**
* 更新选中计数
*/
updateSelectedCount() {
const countLabel = document.getElementById('selectedCount');
countLabel.textContent = `已选择 ${this.selectedImages.size} / ${this.imageData.length} 张图片`;
}
/**
* 下载选中的图片
*/
async downloadSelected() {
if (this.selectedImages.size === 0) {
alert('请先选择要下载的图片');
return;
}
this.showProgress();
const selectedImageData = this.imageData.filter(img => this.selectedImages.has(img.id));
for (let i = 0; i < selectedImageData.length; i++) {
const imageInfo = selectedImageData[i];
const filename = `${this.formattedDate}_${this.generateRandomString()}.jpg`;
try {
this.downloadImageByGM(imageInfo.url, filename);
this.updateProgress(i + 1, selectedImageData.length, `正在下载第 ${i + 1} 张图片...`);
// 添加延迟避免过快下载
await new Promise(resolve => setTimeout(resolve, 200));
} catch (error) {
console.error(`下载图片失败: ${imageInfo.url}`, error);
}
}
this.updateProgress(selectedImageData.length, selectedImageData.length, '下载完成!');
setTimeout(() => {
this.hideProgress();
this.hideModal();
}, 2000);
}
/**
* 打包下载为ZIP
*/
async downloadAsZip() {
if (this.selectedImages.size === 0) {
alert('请先选择要下载的图片');
return;
}
if (typeof JSZip === 'undefined') {
alert('ZIP功能不可用,请使用普通下载');
return;
}
this.showProgress();
const zip = new JSZip();
const selectedImageData = this.imageData.filter(img => this.selectedImages.has(img.id));
for (let i = 0; i < selectedImageData.length; i++) {
const imageInfo = selectedImageData[i];
const filename = `image_${i + 1}_${this.generateRandomString()}.jpg`;
try {
this.updateProgress(i + 1, selectedImageData.length, `正在处理第 ${i + 1} 张图片...`);
const response = await fetch(imageInfo.url);
const blob = await response.blob();
zip.file(filename, blob);
} catch (error) {
console.error(`处理图片失败: ${imageInfo.url}`, error);
}
}
this.updateProgress(selectedImageData.length, selectedImageData.length, '正在生成ZIP文件...');
try {
const zipBlob = await zip.generateAsync({type: 'blob'});
const link = document.createElement('a');
link.href = URL.createObjectURL(zipBlob);
link.download = `images_${this.formattedDate}.zip`;
link.click();
this.updateProgress(selectedImageData.length, selectedImageData.length, 'ZIP文件下载完成!');
} catch (error) {
console.error('生成ZIP文件失败:', error);
alert('生成ZIP文件失败,请使用普通下载');
}
setTimeout(() => {
this.hideProgress();
this.hideModal();
}, 2000);
}
/**
* 显示进度条
*/
showProgress() {
const progressContainer = document.getElementById('progressContainer');
progressContainer.style.display = 'block';
}
/**
* 隐藏进度条
*/
hideProgress() {
const progressContainer = document.getElementById('progressContainer');
progressContainer.style.display = 'none';
}
/**
* 更新进度
*/
updateProgress(current, total, message) {
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
const percentage = Math.round((current / total) * 100);
progressFill.style.width = `${percentage}%`;
progressText.textContent = `${message} (${current}/${total})`;
}
/**
* 隐藏模态框
*/
hideModal() {
this.modal.style.display = 'none';
this.selectedImages.clear();
}
/**
* 添加按钮点击事件监听器
* @param {Function} handler 点击处理函数
*/
addClickListener(handler) {
this.downloadButton.addEventListener('click', handler);
}
/**
* 根据当前网站设置下载处理器
*/
setupDownloadHandler() {
const currentUrl = window.location.href;
switch (true) {
case currentUrl.includes("mp.weixin.qq.com/s"):
this.setupWeixinHandler();
break;
case currentUrl.includes("https://weibo.com/"):
this.setupWeiboHandler();
break;
case currentUrl.includes("https://m.weibo.cn/"):
this.setupWeiboMobileHandler();
break;
case currentUrl.includes("https://www.threads.com/"):
this.setupThreadsHandler();
break;
default:
this.setupUniversalHandler();
}
}
/**
* 微信公众号处理器
*/
setupWeixinHandler() {
// 移除二维码
const qrCode = document.getElementById('js_pc_qr_code');
if (qrCode) {
qrCode.remove();
}
this.addClickListener(() => {
const articleElement = document.querySelector("#js_article");
if (articleElement) {
if (articleElement.classList.contains('share_content_page')) {
this.imgElements = document.querySelectorAll('.swiper_item img');
} else {
this.imgElements = document.querySelectorAll('#img-content img');
}
} else {
this.imgElements = document.querySelectorAll('img');
}
this.batchDownload(this.imgElements);
});
}
/**
* 微博处理器
*/
setupWeiboHandler() {
this.addClickListener(() => {
console.clear();
console.group("微博图片下载");
this.imgElements = document.querySelectorAll(".Viewer_prevItem_McSJ4 img");
// URL处理函数:替换为大图
const urlProcessor = (url) => url.replace("orj360", "large");
this.batchDownload(this.imgElements, urlProcessor, true);
console.groupEnd();
});
}
/**
* 微博手机版处理器
*/
setupWeiboMobileHandler() {
this.addClickListener(() => {
console.clear();
console.group("微博手机版图片下载");
this.imgElements = document.querySelectorAll("img[data-v-5deaae85]");
// URL处理函数:替换为大图
const urlProcessor = (url) => url.replace("orj360", "large");
this.batchDownload(this.imgElements, urlProcessor, true);
console.groupEnd();
});
}
/**
* Threads处理器
*/
setupThreadsHandler() {
this.addClickListener(() => {
console.clear();
console.group("Threads图片下载");
const elements = document.querySelectorAll('picture.x87ps6o');
const imageUrls = [];
Array.from(elements).forEach(element => {
try {
if (element.firstChild && element.firstChild.srcset) {
const url = element.firstChild.srcset.split(' ')[0];
if (url && (url.startsWith('http://') || url.startsWith('https://'))) {
imageUrls.push(url);
console.log(`已获取图片URL: ${url}`);
} else {
console.warn(`无效的图片URL: ${url}`);
}
}
} catch (error) {
console.error('处理元素时出错:', error);
}
});
// 下载所有图片
imageUrls.forEach(url => {
this.downloadImageByXHR(url, `${this.formattedDate}_${this.generateRandomString()}.jpg`);
});
console.log(`开始下载 ${imageUrls.length} 张图片`);
console.groupEnd();
});
}
/**
* 通用处理器
*/
setupUniversalHandler() {
this.addClickListener(() => {
this.imgElements = document.querySelectorAll('img');
this.batchDownload(this.imgElements);
});
}
}
/**
* 页面加载完成后初始化
*/
window.addEventListener('load', () => {
new ImageDownloader();
});
})();