您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
可自定义父元素选择器,提取指定元素内的图片链接并导出为PDF
// ==UserScript== // @name 图片链接提取与PDF导出通用版 // @namespace http://tampermonkey.net/ // @version 3.0 // @description 可自定义父元素选择器,提取指定元素内的图片链接并导出为PDF // @author TedLife // @homepageURL https://tedlife.com/ // @license MIT // @match *://*/* // @grant GM_setClipboard // @grant GM_notification // @grant GM_getResourceText // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @require https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js // @resource pdfCSS https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css // @connect * // ==/UserScript== (function() { 'use strict'; // 默认配置 const defaultConfig = { selector: '.EaCvy', selectorType: 'class' // 'class', 'id', 'custom' }; // 获取或设置配置 function getConfig() { const saved = GM_getValue('imgExtractorConfig', null); return saved ? JSON.parse(saved) : defaultConfig; } function saveConfig(config) { GM_setValue('imgExtractorConfig', JSON.stringify(config)); } // 当前配置 let currentConfig = getConfig(); // 添加PDF生成所需的Bootstrap样式 const pdfCSS = GM_getResourceText('pdfCSS'); GM_addStyle(` /* 浮动按钮样式 */ #pdf-extractor-btn { position: fixed; bottom: 80px; right: 20px; z-index: 9999; padding: 12px 20px; background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%); color: white; border: none; border-radius: 50px; cursor: pointer; font-weight: bold; box-shadow: 0 6px 20px rgba(37, 117, 252, 0.5); transition: all 0.3s ease; display: flex; align-items: center; gap: 8px; } #pdf-extractor-btn:hover { transform: translateY(-3px); box-shadow: 0 8px 25px rgba(37, 117, 252, 0.7); } #pdf-extractor-btn:active { transform: translateY(1px); } #pdf-extractor-btn .spinner { display: none; width: 16px; height: 16px; border: 3px solid rgba(255,255,255,0.3); border-radius: 50%; border-top-color: white; animation: spin 1s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } /* 结果面板样式 */ #pdf-extractor-container { position: fixed; bottom: 20px; right: 20px; z-index: 9998; background: white; border-radius: 15px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); padding: 20px; width: 400px; max-height: 70vh; overflow: auto; display: none; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } #pdf-extractor-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid #eee; } #pdf-extractor-title { margin: 0; color: #2c3e50; font-size: 1.4rem; } #pdf-extractor-close { background: none; border: none; font-size: 24px; cursor: pointer; color: #777; padding: 0 10px; } .image-result-item { display: flex; padding: 12px; border-bottom: 1px solid #f1f1f1; transition: background 0.2s; } .image-result-item:hover { background: #f9f9ff; } .image-preview { width: 70px; height: 70px; object-fit: cover; border-radius: 8px; margin-right: 15px; border: 1px solid #eee; background: #f8f8f8; } .image-details { flex: 1; overflow: hidden; } .image-url { display: block; word-break: break-all; font-size: 0.85rem; color: #3498db; text-decoration: none; margin-bottom: 8px; } .image-url:hover { text-decoration: underline; } .image-actions { display: flex; gap: 8px; } .action-btn { padding: 5px 12px; border-radius: 4px; font-size: 0.85rem; cursor: pointer; border: none; background: #f1f5ff; color: #2575fc; transition: all 0.2s; } .action-btn:hover { background: #e1ebff; transform: translateY(-1px); } .action-btn.copy { background: #e8f7f0; color: #27ae60; } .action-btn.copy:hover { background: #d1f2e5; } .summary-row { display: flex; justify-content: space-between; align-items: center; margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee; } .image-count { font-weight: bold; color: #2c3e50; } #generate-pdf-btn { padding: 10px 20px; background: linear-gradient(135deg, #ff6b6b 0%, #ff8e53 100%); color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; transition: all 0.3s; display: flex; align-items: center; gap: 8px; } #generate-pdf-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4); } #generate-pdf-btn .spinner { display: none; width: 16px; height: 16px; border: 3px solid rgba(255,255,255,0.3); border-radius: 50%; border-top-color: white; animation: spin 1s linear infinite; } .no-images { text-align: center; padding: 30px 0; color: #777; } .no-images i { font-size: 3rem; color: #e0e0e0; margin-bottom: 15px; display: block; } /* 加载动画 */ .loading-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.9); display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 10000; display: none; } .loading-spinner { width: 60px; height: 60px; border: 5px solid #f3f3f3; border-top: 5px solid #3498db; border-radius: 50%; animation: spin 1.5s linear infinite; margin-bottom: 20px; } .loading-text { font-size: 1.2rem; color: #2c3e50; font-weight: 500; } .progress-container { width: 300px; max-width: 80%; background: #f1f1f1; border-radius: 10px; margin-top: 20px; overflow: hidden; } .progress-bar { height: 20px; background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%); border-radius: 10px; width: 0%; transition: width 0.3s ease; } /* 设置面板样式 */ #pdf-settings-container { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10001; background: white; border-radius: 15px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); padding: 30px; width: 500px; max-width: 90vw; display: none; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } #pdf-settings-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 10000; display: none; } .settings-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; padding-bottom: 15px; border-bottom: 2px solid #f0f0f0; } .settings-title { margin: 0; color: #2c3e50; font-size: 1.5rem; } .settings-close { background: none; border: none; font-size: 28px; cursor: pointer; color: #777; padding: 0 10px; } .form-group { margin-bottom: 20px; } .form-label { display: block; margin-bottom: 8px; font-weight: 600; color: #2c3e50; } .form-input { width: 100%; padding: 12px 15px; border: 2px solid #e1e8ed; border-radius: 8px; font-size: 14px; transition: border-color 0.3s; } .form-input:focus { outline: none; border-color: #2575fc; } .form-select { width: 100%; padding: 12px 15px; border: 2px solid #e1e8ed; border-radius: 8px; font-size: 14px; background: white; cursor: pointer; } .form-help { font-size: 12px; color: #666; margin-top: 5px; } .settings-buttons { display: flex; gap: 15px; justify-content: flex-end; margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; } .btn-save { padding: 12px 25px; background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%); color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; transition: all 0.3s; } .btn-save:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(39, 174, 96, 0.4); } .btn-cancel { padding: 12px 25px; background: #f8f9fa; color: #6c757d; border: 2px solid #e9ecef; border-radius: 8px; cursor: pointer; font-weight: bold; transition: all 0.3s; } .btn-cancel:hover { background: #e9ecef; border-color: #dee2e6; } .btn-settings { position: fixed; bottom: 140px; right: 20px; z-index: 9999; padding: 10px; background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); color: white; border: none; border-radius: 50%; cursor: pointer; width: 45px; height: 45px; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 15px rgba(231, 76, 60, 0.4); transition: all 0.3s ease; } .btn-settings:hover { transform: translateY(-3px); box-shadow: 0 6px 20px rgba(231, 76, 60, 0.6); } `); // 创建UI元素 const container = document.createElement('div'); container.id = 'pdf-extractor-container'; container.innerHTML = ` <div id="pdf-extractor-header"> <h3 id="pdf-extractor-title">图片链接提取结果</h3> <button id="pdf-extractor-close">×</button> </div> <div id="image-link-results"></div> <div class="summary-row"> <div class="image-count">找到 <span id="image-count">0</span> 张图片</div> <button id="generate-pdf-btn"> <span class="spinner"></span> 导出为PDF </button> </div> `; const toggleBtn = document.createElement('button'); toggleBtn.id = 'pdf-extractor-btn'; toggleBtn.innerHTML = ` <span class="spinner"></span> <span class="btn-text">提取图片链接</span> `; // 创建设置按钮 const settingsBtn = document.createElement('button'); settingsBtn.className = 'btn-settings'; settingsBtn.innerHTML = '⚙️'; settingsBtn.title = '设置选择器'; // 创建设置面板 const settingsOverlay = document.createElement('div'); settingsOverlay.id = 'pdf-settings-overlay'; const settingsContainer = document.createElement('div'); settingsContainer.id = 'pdf-settings-container'; settingsContainer.innerHTML = ` <div class="settings-header"> <h3 class="settings-title">选择器设置</h3> <button class="settings-close">×</button> </div> <form id="settings-form"> <div class="form-group"> <label class="form-label">选择器类型</label> <select class="form-select" id="selector-type"> <option value="class">CSS类名 (class)</option> <option value="id">元素ID</option> <option value="custom">自定义选择器</option> </select> <div class="form-help">选择要匹配的元素类型</div> </div> <div class="form-group"> <label class="form-label">选择器值</label> <input type="text" class="form-input" id="selector-value" placeholder="例如:EaCvy"> <div class="form-help" id="selector-help">输入CSS类名(不包含点号)</div> </div> <div class="form-group"> <label class="form-label">预览选择器</label> <input type="text" class="form-input" id="selector-preview" readonly> <div class="form-help">这是最终使用的CSS选择器</div> </div> <div class="settings-buttons"> <button type="button" class="btn-cancel">取消</button> <button type="submit" class="btn-save">保存设置</button> </div> </form> `; // 创建加载覆盖层 const loadingOverlay = document.createElement('div'); loadingOverlay.className = 'loading-overlay'; loadingOverlay.innerHTML = ` <div class="loading-spinner"></div> <div class="loading-text">正在生成PDF文件,请稍候...</div> <div class="progress-container"> <div class="progress-bar" id="pdf-progress-bar"></div> </div> <div id="progress-text">0%</div> `; // 添加到文档 document.body.appendChild(container); document.body.appendChild(toggleBtn); document.body.appendChild(settingsBtn); document.body.appendChild(settingsOverlay); document.body.appendChild(settingsContainer); document.body.appendChild(loadingOverlay); // 设置面板相关函数 function updateSelectorPreview() { const type = document.getElementById('selector-type').value; const value = document.getElementById('selector-value').value.trim(); const preview = document.getElementById('selector-preview'); const help = document.getElementById('selector-help'); if (!value) { preview.value = ''; return; } switch (type) { case 'class': preview.value = `.${value} img`; help.textContent = '输入CSS类名(不包含点号)'; break; case 'id': preview.value = `#${value} img`; help.textContent = '输入元素ID(不包含井号)'; break; case 'custom': preview.value = `${value} img`; help.textContent = '输入完整的CSS选择器'; break; } } function loadSettingsToForm() { const config = getConfig(); document.getElementById('selector-type').value = config.selectorType; // 从选择器中提取值 let value = ''; if (config.selectorType === 'class' && config.selector.startsWith('.')) { value = config.selector.replace(/\s+img$/, '').substring(1); } else if (config.selectorType === 'id' && config.selector.startsWith('#')) { value = config.selector.replace(/\s+img$/, '').substring(1); } else if (config.selectorType === 'custom') { value = config.selector.replace(/\s+img$/, ''); } document.getElementById('selector-value').value = value; updateSelectorPreview(); } function showSettings() { loadSettingsToForm(); settingsOverlay.style.display = 'block'; settingsContainer.style.display = 'block'; } function hideSettings() { settingsOverlay.style.display = 'none'; settingsContainer.style.display = 'none'; } // 事件处理 toggleBtn.addEventListener('click', function() { extractImageLinks(); container.style.display = container.style.display === 'block' ? 'none' : 'block'; toggleBtn.querySelector('.btn-text').textContent = container.style.display === 'block' ? '隐藏结果' : '提取图片链接'; }); document.getElementById('pdf-extractor-close').addEventListener('click', function() { container.style.display = 'none'; toggleBtn.querySelector('.btn-text').textContent = '提取图片链接'; }); document.getElementById('generate-pdf-btn').addEventListener('click', generatePDF); // 设置面板事件处理 settingsBtn.addEventListener('click', showSettings); settingsOverlay.addEventListener('click', hideSettings); settingsContainer.addEventListener('click', function(e) { e.stopPropagation(); // 防止点击面板内容时关闭 }); document.querySelector('.settings-close').addEventListener('click', hideSettings); document.querySelector('.btn-cancel').addEventListener('click', hideSettings); // 选择器类型和值变化时更新预览 document.getElementById('selector-type').addEventListener('change', updateSelectorPreview); document.getElementById('selector-value').addEventListener('input', updateSelectorPreview); // 设置表单提交 document.getElementById('settings-form').addEventListener('submit', function(e) { e.preventDefault(); const type = document.getElementById('selector-type').value; const value = document.getElementById('selector-value').value.trim(); if (!value) { GM_notification({ title: '设置错误', text: '请输入选择器值', timeout: 3000 }); return; } let selector; switch (type) { case 'class': selector = `.${value}`; break; case 'id': selector = `#${value}`; break; case 'custom': selector = value; break; } // 测试选择器是否有效 try { document.querySelectorAll(`${selector} img`); } catch (error) { GM_notification({ title: '选择器错误', text: '无效的CSS选择器,请检查语法', timeout: 3000 }); return; } // 保存配置 currentConfig = { selector: selector, selectorType: type }; saveConfig(currentConfig); hideSettings(); GM_notification({ title: '设置已保存', text: `选择器已更新为: ${selector} img`, timeout: 3000 }); }); // 初始化设置面板显示 setTimeout(() => { loadSettingsToForm(); }, 100); // 提取图片链接函数 function extractImageLinks() { const config = getConfig(); const selector = `${config.selector} img`; const elements = document.querySelectorAll(selector); const resultsContainer = document.getElementById('image-link-results'); const countElement = document.getElementById('image-count'); resultsContainer.innerHTML = ''; if (elements.length === 0) { resultsContainer.innerHTML = ` <div class="no-images"> <div>🖼️</div> <p>没有找到符合条件的图片</p> <p>当前选择器: <code>${selector}</code></p> <p>点击右下角的⚙️按钮可以修改选择器设置</p> </div> `; countElement.textContent = '0'; return; } const fragment = document.createDocumentFragment(); let validImageCount = 0; elements.forEach(img => { // 优先获取data-src(用于懒加载),没有则使用src const src = img.dataset.src || img.src; if (!src) return; validImageCount++; const item = document.createElement('div'); item.className = 'image-result-item'; const imgPreview = document.createElement('img'); imgPreview.className = 'image-preview'; imgPreview.src = src; imgPreview.onerror = function() { this.src = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="70" height="70" viewBox="0 0 24 24" fill="none" stroke="%23ccc" stroke-width="1"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>'; }; const details = document.createElement('div'); details.className = 'image-details'; const url = document.createElement('a'); url.className = 'image-url'; url.href = src; url.textContent = src; url.target = '_blank'; const actions = document.createElement('div'); actions.className = 'image-actions'; const copyBtn = document.createElement('button'); copyBtn.className = 'action-btn copy'; copyBtn.textContent = '复制链接'; copyBtn.addEventListener('click', () => { GM_setClipboard(src); GM_notification({ text: '图片链接已复制到剪贴板', timeout: 2000 }); }); actions.appendChild(copyBtn); details.appendChild(url); details.appendChild(actions); item.appendChild(imgPreview); item.appendChild(details); fragment.appendChild(item); }); resultsContainer.appendChild(fragment); countElement.textContent = validImageCount; // 显示通知 if (validImageCount > 0) { GM_notification({ title: '图片链接提取完成', text: `成功提取了 ${validImageCount} 张图片的链接`, timeout: 3000 }); } } // 生成PDF函数(优化版) function generatePDF() { const btn = document.getElementById('generate-pdf-btn'); const btnSpinner = btn.querySelector('.spinner'); const config = getConfig(); const selector = `${config.selector} img`; const images = Array.from(document.querySelectorAll(selector)); const validImages = images.filter(img => img.dataset.src || img.src); if (validImages.length === 0) { GM_notification({ title: '生成PDF失败', text: '没有找到可用的图片', timeout: 3000 }); return; } // 显示加载状态 loadingOverlay.style.display = 'flex'; btnSpinner.style.display = 'inline-block'; btn.disabled = true; // 获取进度元素 const progressBar = document.getElementById('pdf-progress-bar'); const progressText = document.querySelector('#progress-text'); // 创建PDF文档 const { jsPDF } = window.jspdf; const doc = new jsPDF(); // 存储所有图片的Base64数据 const imageDataPromises = []; let processedCount = 0; // 获取所有图片的Base64数据 validImages.forEach(img => { const src = img.dataset.src || img.src; const promise = new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url: src, responseType: "blob", onload: function(response) { const reader = new FileReader(); reader.onloadend = function() { processedCount++; const progress = Math.round((processedCount / validImages.length) * 100); progressBar.style.width = `${progress}%`; progressText.textContent = `${progress}%`; resolve({ data: reader.result, width: img.naturalWidth || img.width, height: img.naturalHeight || img.height }); }; reader.readAsDataURL(response.response); }, onerror: function() { processedCount++; resolve(null); // 忽略加载失败的图片 } }); }); imageDataPromises.push(promise); }); // 等待所有图片加载完成 Promise.all(imageDataPromises).then(imageDataArray => { // 过滤掉加载失败的图片 const validImageData = imageDataArray.filter(data => data !== null); if (validImageData.length === 0) { loadingOverlay.style.display = 'none'; btnSpinner.style.display = 'none'; btn.disabled = false; GM_notification({ title: '生成PDF失败', text: '所有图片加载失败', timeout: 3000 }); return; } // 添加图片到PDF validImageData.forEach((imageData, index) => { const imgWidth = imageData.width; const imgHeight = imageData.height; // 计算图片在PDF中的尺寸(保持比例) const pageWidth = doc.internal.pageSize.getWidth(); const pageHeight = doc.internal.pageSize.getHeight(); // 计算缩放比例(保留5%的边距) const margin = pageWidth * 0.05; const maxWidth = pageWidth - (margin * 2); const maxHeight = pageHeight - (margin * 2); let widthRatio = maxWidth / imgWidth; let heightRatio = maxHeight / imgHeight; let ratio = Math.min(widthRatio, heightRatio); const scaledWidth = imgWidth * ratio; const scaledHeight = imgHeight * ratio; // 计算居中位置 const x = (pageWidth - scaledWidth) / 2; const y = (pageHeight - scaledHeight) / 2; // 添加新页面(第一页除外) if (index > 0) { doc.addPage(); } // 添加图片(不添加任何文字) doc.addImage( imageData.data, 'JPEG', x, y, scaledWidth, scaledHeight ); }); // 保存PDF const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); doc.save(`extracted-images-${timestamp}.pdf`); // 隐藏加载状态 loadingOverlay.style.display = 'none'; btnSpinner.style.display = 'none'; btn.disabled = false; GM_notification({ title: 'PDF导出成功', text: `已导出 ${validImageData.length} 张图片`, timeout: 3000 }); }); } })();