// ==UserScript== // @name 网页图片采集器 // @namespace http://tampermonkey.net/ // @version 4.1 // @description 下载网页中的图片 // @author YourName // @match *://*/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_setClipboard // @grant GM_notification // @grant GM_download // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js // @icon https://cdn-icons-png.flaticon.com/512/2107/2107957.png // @license MIT // ==/UserScript== (function() { 'use strict'; const CONFIG = { buttonSize: 30, activeColor: '#e74c3c', hoverColor: '#c0392b', zIndex: 99999, positionOffset: 25, touchDelay: 300, supportFormats: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'tiff'], maxPreviewSize: 50, loadTimeout: 5000, infoTruncateLength: 4, panelSafeMargin: '20px', panelMinSize: '320px', // 后缀补全配置:无后缀时默认添加的格式 defaultImageFormat: 'png' }; GM_addStyle(` /*按钮样式 */ .radar-container {position:fixed;z-index:${CONFIG.zIndex};cursor:move;transition:transform 0.2s;touch-action:none;} .radar-button {width:${CONFIG.buttonSize}px;height:${CONFIG.buttonSize}px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#e74c3c,#922b21);box-shadow:0 6px 18px rgba(0,0,0,0.3),0 0 0 4px rgba(255,255,255,0.15),inset 0 0 12px rgba(0,0,0,0.3);cursor:pointer;border:none;outline:none;position:relative;overflow:hidden;user-select:none;-webkit-tap-highlight-color:transparent;animation:pulse 2s infinite;transition:transform 0.3s,box-shadow 0.3s;} .radar-button:hover {transform:scale(1.05);box-shadow:0 8px 22px rgba(0,0,0,0.4),0 0 0 4px rgba(255,255,255,0.25),inset 0 0 15px rgba(0,0,0,0.4);} .radar-button:active {transform:scale(0.95);} .radar-icon {width:24px;height:24px;position:relative;display:flex;justify-content:center;align-items:center;filter:drop-shadow(0 0 2px rgba(255,255,255,0.5));animation:radar-scan 4s linear infinite;} .radar-icon svg {width:100%;height:100%;} /* 面板核心样式:自适应视口 */ #svgSnifferModal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; margin: ${CONFIG.panelSafeMargin}; max-width: calc(100vw - 2 * ${CONFIG.panelSafeMargin}); max-height: calc(100vh - 2 * ${CONFIG.panelSafeMargin}); min-width: ${CONFIG.panelMinSize}; min-height: ${CONFIG.panelMinSize}; width: auto; height: auto; z-index: 10000; background: #fff; border-radius: 12px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); border: 1px solid #ddd; display: none; flex-direction: column; font-family: Arial, sans-serif; overflow: hidden; } /* 面板头部 */ .modal-header { padding: 12px 20px; background:linear-gradient(135deg,#e74c3c,#922b21); color: #fff; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; } .modal-header h2 {margin: 0;font-size: 1.2rem;font-weight: 600;} .close-btn { background: none; border: none; color: #fff; font-size: 1.5rem; cursor: pointer; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: background 0.2s; } .close-btn:hover {background: rgba(255,255,255,0.2);} /* 操作栏 */ .action-bar { padding: 10px 20px; background: #f8f9fa; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; flex-wrap: wrap; gap: 10px; } .select-all-control { display: flex; align-items: center; gap: 8px; font-size: 0.9rem; } .action-buttons { display: flex; gap: 8px; flex-wrap: wrap; } .action-btn { padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 0.9rem; transition: all 0.2s; white-space: nowrap; } .download-btn {background: #27ae60;color: #fff;} .download-btn:hover {background: #219653;transform: translateY(-2px);} .copy-btn {background: #2980b9;color: #fff;} .copy-btn:hover {background: #2573a7;transform: translateY(-2px);} /* 内容区 */ .modal-content { padding: 15px; overflow-y: auto; flex-grow: 1; min-height: 0; } /* 图片项 */ .svg-item { display: flex; align-items: center; padding: 12px; border-bottom: 1px solid #eee; transition: background 0.2s; justify-content: space-between; gap: 10px; min-width: 0; } .svg-item:hover {background: #f8fafc;} .svg-checkbox { width: 18px; height: 18px; cursor: pointer; flex-shrink: 0; } .svg-preview { width: ${CONFIG.maxPreviewSize}px; height: ${CONFIG.maxPreviewSize}px; border: 1px solid #e0e0e0; display: flex; align-items: center; justify-content: center; border-radius: 6px; background-image: linear-gradient(45deg, #e0e0e0 25%, transparent 25%), linear-gradient(-45deg, #e0e0e0 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #e0e0e0 75%), linear-gradient(-45deg, transparent 75%, #e0e0e0 75%); background-size: 10px 10px; background-position: 0 0, 0 5px, 5px -5px, -5px 0px; background-color: #f8f8f8; box-shadow: 0 2px 6px rgba(0,0,0,0.1); overflow: hidden; flex-shrink: 0; } .svg-preview img, .svg-preview svg { max-width: 100%; max-height: 100%; object-fit: contain; display: block; } /* 图片信息 */ .svg-info { flex-grow: 1; min-width: 0; } .svg-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 1rem; color: #2c3e50; margin-bottom: 3px; max-width: 80px; } .svg-meta { font-size: 0.8rem; color: #6e6e73; display: flex; gap: 12px; flex-wrap: wrap; } .svg-meta span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 80px; } /* 单独下载按钮 */ .item-download-btn { padding: 5px 10px; background: #e74c3c; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 0.8rem; transition: background 0.2s; white-space: nowrap; flex-shrink: 0; } .item-download-btn:hover {background: #c0392b;} /* 其他基础样式 */ .overlay {display: none;position: fixed;top: 0;left: 0;width: 100%;height: 100%;background: rgba(0,0,0,0.5);z-index: 9999;} .loading {text-align: center;padding: 25px;font-size: 1.1rem;color: #666;} .copy-notification { position: fixed; top: 30px; left: 50%; transform: translateX(-50%); background: #27ae60; color: #fff; padding: 10px 20px; border-radius: 6px; z-index: 100000; opacity: 0; transition: opacity 0.5s; pointer-events: none; white-space: nowrap; font-weight: 500; } /* 动画样式 */ @keyframes radar-scan {0% {transform: rotate(0deg);} 100% {transform: rotate(360deg);}} @keyframes pulse {0% {box-shadow: 0 0 0 0 rgba(231,76,60,0.6);} 70% {box-shadow: 0 0 0 12px rgba(231,76,60,0);} 100% {box-shadow: 0 0 0 0 rgba(231,76,60,0);}} .temp-visible-for-scan {display: block !important;visibility: visible !important;opacity: 1 !important;position: absolute !important;top: -9999px !important;left: -9999px !important;width: auto !important;height: auto !important;} `); const radarContainer = document.createElement('div'); radarContainer.className = 'radar-container'; radarContainer.id = 'radarContainer'; const radarButton = document.createElement('div'); radarButton.className = 'radar-button'; radarButton.id = 'radarButton'; radarButton.innerHTML = `
`; radarContainer.appendChild(radarButton); document.body.appendChild(radarContainer); const svgModal = document.createElement('div'); svgModal.id = 'svgSnifferModal'; svgModal.innerHTML = `
`; document.body.appendChild(svgModal); const overlay = document.createElement('div'); overlay.className = 'overlay'; document.body.appendChild(overlay); const copyNotification = document.createElement('div'); copyNotification.className = 'copy-notification'; document.body.appendChild(copyNotification); let globalImageItems = []; let imageItemCache = new Map(); let isDragging = false; let startX, startY, startLeft, startTop; let dragStartTime = 0; let touchTimer = null; let blobUrls = []; let tempVisibleElements = []; // 4字节截断工具函数 function truncateTo4Bytes(text) { if (!text || typeof text !== 'string') return ''; let byteCount = 0; let result = ''; for (let i = 0; i < text.length; i++) { const char = text[i]; const charBytes = /[\u4e00-\u9fa5]/.test(char) ? 2 : 1; if (byteCount + charBytes > CONFIG.infoTruncateLength) { break; } result += char; byteCount += charBytes; if (byteCount === CONFIG.infoTruncateLength) { break; } } return result; } // ========== 新增:文件后缀处理工具函数 ========== /** * 处理文件名,无图片后缀时自动添加默认格式(.png) * @param {string} fileName - 原始文件名 * @param {string} originalFormat - 图片原始格式(可能为空) * @returns {string} 补全后缀后的完整文件名 */ function completeImageSuffix(fileName, originalFormat) { // 1. 提取现有后缀 const extMatch = fileName.match(/\.([^.]+)$/); const hasValidSuffix = extMatch ? CONFIG.supportFormats.includes(extMatch[1].toLowerCase()) : false; // 2. 已有有效后缀,直接返回 if (hasValidSuffix) return fileName; // 3. 无有效后缀,使用原始格式或默认格式(.png) const targetFormat = (originalFormat && CONFIG.supportFormats.includes(originalFormat.toLowerCase())) ? originalFormat.toLowerCase() : CONFIG.defaultImageFormat; // 4. 避免重复添加后缀(如文件名已含".但无后缀") return fileName.endsWith('.') ? `${fileName}${targetFormat}` : `${fileName}.${targetFormat}`; } // ========== 工具函数结束 ========== // 处理SVG内容使其在预览框中正确显示 function processSVGForPreview(svgContent) { try { const parser = new DOMParser(); const doc = parser.parseFromString(svgContent, 'image/svg+xml'); const svgElement = doc.documentElement; const width = svgElement.getAttribute('width') || svgElement.getAttribute('viewBox')?.split(' ')[2] || CONFIG.maxPreviewSize; const height = svgElement.getAttribute('height') || svgElement.getAttribute('viewBox')?.split(' ')[3] || CONFIG.maxPreviewSize; svgElement.setAttribute('width', '100%'); svgElement.setAttribute('height', '100%'); svgElement.setAttribute('preserveAspectRatio', 'xMidYMid meet'); if (!svgElement.hasAttribute('viewBox')) { svgElement.setAttribute('viewBox', `0 0 ${width} ${height}`); } return svgElement.outerHTML; } catch (error) { console.warn('SVG处理失败,使用原始内容:', error); return svgContent; } } // 基础图片嗅探方式 function collectBasicImages() { const images = []; const imgElements = document.querySelectorAll('img'); imgElements.forEach((img, index) => { const src = img.src || img.dataset.src || img.currentSrc; if (src && !src.startsWith('data:')) { const truncatedName = truncateTo4Bytes(img.alt || '未命名图片'); const originalFormat = getFileExtension(src).toLowerCase(); const format = truncateTo4Bytes(originalFormat || CONFIG.defaultImageFormat); images.push({ id: `basic-img-${index}`, url: src, name: truncatedName, format: format, width: img.naturalWidth || img.width || '未知', height: img.naturalHeight || img.height || '未知', type: 'img-tag', preview: src, element: img, originalName: img.alt || '未命名图片', originalFormat: originalFormat, svgContent: '' }); } }); return images; } function initRadarButton() { const domain = location.hostname.replace(/\./g, '-'); const positionKey = `radarPosition_${domain}`; const savedPosition = GM_getValue(positionKey); if (savedPosition) { radarContainer.style.left = `${savedPosition.x}px`; radarContainer.style.top = `${savedPosition.y}px`; } else { radarContainer.style.right = `${CONFIG.positionOffset}px`; radarContainer.style.bottom = `${CONFIG.positionOffset}px`; } radarContainer.addEventListener('mousedown', startDrag); radarContainer.addEventListener('touchstart', startDrag, { passive: false }); radarButton.addEventListener('click', (e) => { if (!isDragging && Date.now() - dragStartTime > CONFIG.touchDelay) { showImageList(); } }); } function startDrag(e) { e.preventDefault(); const clientX = e.clientX || e.touches[0].clientX; const clientY = e.clientY || e.touches[0].clientY; const computedStyle = window.getComputedStyle(radarContainer); startLeft = parseInt(computedStyle.left) || 0; startTop = parseInt(computedStyle.top) || 0; if (computedStyle.right !== 'auto') { const rightPos = parseInt(computedStyle.right); startLeft = window.innerWidth - rightPos - CONFIG.buttonSize; radarContainer.style.right = 'auto'; radarContainer.style.left = `${startLeft}px`; } startX = clientX; startY = clientY; dragStartTime = Date.now(); if (e.type === 'touchstart') { touchTimer = setTimeout(() => { isDragging = true; radarContainer.style.transition = 'none'; }, CONFIG.touchDelay); } else { isDragging = true; } document.addEventListener('mousemove', drag); document.addEventListener('touchmove', drag, { passive: false }); document.addEventListener('mouseup', endDrag); document.addEventListener('touchend', endDrag); } function drag(e) { if (!isDragging) return; e.preventDefault(); const clientX = e.clientX || e.touches[0].clientX; const clientY = e.clientY || e.touches[0].clientY; const dx = clientX - startX; const dy = clientY - startY; radarContainer.style.left = `${startLeft + dx}px`; radarContainer.style.top = `${startTop + dy}px`; radarContainer.style.right = 'auto'; } function endDrag(e) { if (touchTimer) { clearTimeout(touchTimer); touchTimer = null; } if (!isDragging) { if (Date.now() - dragStartTime < CONFIG.touchDelay) { showImageList(); } return; } isDragging = false; radarContainer.style.transition = ''; document.removeEventListener('mousemove', drag); document.removeEventListener('touchmove', drag); document.removeEventListener('mouseup', endDrag); document.removeEventListener('touchend', endDrag); const domain = location.hostname.replace(/\./g, '-'); const positionKey = `radarPosition_${domain}`; const rect = radarContainer.getBoundingClientRect(); GM_setValue(positionKey, { x: rect.left, y: rect.top }); } function tempShowHiddenElements() { tempVisibleElements = []; const hiddenSelectors = [ 'div[style*="display:none"]', 'div[style*="visibility:hidden"]', 'div[style*="opacity:0"]', '.errorpage[style*="display:none"]', '[class*="hidden"]', '[hidden]' ]; hiddenSelectors.forEach(selector => { const elements = document.querySelectorAll(selector); elements.forEach(el => { const originalStyle = { display: el.style.display, visibility: el.style.visibility, opacity: el.style.opacity, position: el.style.position, top: el.style.top, left: el.style.left, width: el.style.width, height: el.style.height, className: el.className }; tempVisibleElements.push({ el, originalStyle }); el.classList.add('temp-visible-for-scan'); el.style.display = ''; el.style.visibility = ''; el.style.opacity = ''; }); }); } function restoreHiddenElements() { tempVisibleElements.forEach(({ el, originalStyle }) => { el.classList.remove('temp-visible-for-scan'); el.style.display = originalStyle.display; el.style.visibility = originalStyle.visibility; el.style.opacity = originalStyle.opacity; el.style.position = originalStyle.position; el.style.top = originalStyle.top; el.style.left = originalStyle.left; el.style.width = originalStyle.width; el.style.height = originalStyle.height; el.className = originalStyle.className; }); tempVisibleElements = []; } async function collectImagesFromCss(cssUrl) { const imageUrls = []; try { const response = await fetch(cssUrl, { headers: { 'Accept': 'text/css,*/*;q=0.1' }, credentials: 'same-origin' }); if (!response.ok) throw new Error(`CSS请求失败: ${response.status}`); const cssText = await response.text(); const bgUrlRegex = /background-image\s*:\s*url\(["']?([^"']+)["']?\)/gi; let match; while ((match = bgUrlRegex.exec(cssText)) !== null) { if (match[1]) { const fullUrl = new URL(match[1], cssUrl).href; const ext = getFileExtension(fullUrl).toLowerCase(); if (CONFIG.supportFormats.includes(ext)) { imageUrls.push(fullUrl); } } } } catch (e) { console.warn('采集CSS中的图片失败:', cssUrl, e); } return imageUrls; } async function collectAllCssResources() { const cssUrls = []; const linkElements = document.querySelectorAll('link[rel="stylesheet"]'); linkElements.forEach(link => { const href = link.getAttribute('href'); if (href) { cssUrls.push(new URL(href, window.location.href).href); } }); const styleElements = document.querySelectorAll('style'); styleElements.forEach(style => { const bgUrlRegex = /background-image\s*:\s*url\(["']?([^"']+)["']?\)/gi; let match; while ((match = bgUrlRegex.exec(style.textContent)) !== null) { if (match[1]) { const fullUrl = new URL(match[1], window.location.href).href; const ext = getFileExtension(fullUrl).toLowerCase(); if (CONFIG.supportFormats.includes(ext)) { cssUrls.push(`inline:${fullUrl}`); } } } }); const allImageUrls = []; for (const cssUrl of cssUrls) { if (cssUrl.startsWith('inline:')) { allImageUrls.push(cssUrl.replace('inline:', '')); } else { const imagesFromCss = await collectImagesFromCss(cssUrl); allImageUrls.push(...imagesFromCss); } } return allImageUrls; } async function collectImages() { const imageItems = []; const processedUrls = new Set(); let cssImageUrls = []; const basicImages = collectBasicImages(); basicImages.forEach(img => { if (!processedUrls.has(img.url)) { processedUrls.add(img.url); imageItems.push(img); } }); try { cssImageUrls = await collectAllCssResources(); } catch (e) { console.warn('CSS图片采集异常:', e); } tempShowHiddenElements(); try { // 增强的img标签采集 const imgElements = document.querySelectorAll('img'); for (const img of imgElements) { try { let imgUrl = img.src || img.dataset.src || img.dataset.original || img.currentSrc; if (!imgUrl || processedUrls.has(imgUrl) || imgUrl.startsWith('data:')) continue; const fullUrl = new URL(imgUrl, window.location.href).href; const originalFormat = getFileExtension(fullUrl).toLowerCase(); if (!CONFIG.supportFormats.includes(originalFormat) && originalFormat) continue; if (processedUrls.has(fullUrl)) continue; const originalName = getImageName(fullUrl, img.alt); const truncatedName = truncateTo4Bytes(originalName); const truncatedFormat = truncateTo4Bytes(originalFormat || CONFIG.defaultImageFormat); let svgContent = ''; if (originalFormat === 'svg') { try { const svgResponse = await fetch(fullUrl); if (svgResponse.ok) { svgContent = await svgResponse.text(); } } catch (svgErr) { console.warn('获取SVG原始内容失败:', svgErr); } } const imgInfo = await new Promise((resolve) => { const timer = setTimeout(() => { resolve({ id: `img-${Date.now()}-${Math.random().toString(36).slice(2)}`, url: fullUrl, name: truncatedName, format: truncatedFormat, width: img.width || '未知', height: img.height || '未知', type: 'img-tag', preview: fullUrl, originalName: originalName, originalFormat: originalFormat, svgContent: svgContent }); }, CONFIG.loadTimeout); if (img.complete) { clearTimeout(timer); resolve({ id: `img-${Date.now()}-${Math.random().toString(36).slice(2)}`, url: fullUrl, name: truncatedName, format: truncatedFormat, width: img.naturalWidth || img.width || '未知', height: img.naturalHeight || img.height || '未知', type: 'img-tag', preview: fullUrl, originalName: originalName, originalFormat: originalFormat, svgContent: svgContent }); } else { img.onload = () => { clearTimeout(timer); resolve({ id: `img-${Date.now()}-${Math.random().toString(36).slice(2)}`, url: fullUrl, name: truncatedName, format: truncatedFormat, width: img.naturalWidth || img.width || '未知', height: img.naturalHeight || img.height || '未知', type: 'img-tag', preview: fullUrl, originalName: originalName, originalFormat: originalFormat, svgContent: svgContent }); }; img.onerror = () => { clearTimeout(timer); resolve(null); }; } }); if (imgInfo) { processedUrls.add(imgUrl); imageItems.push(imgInfo); } } catch (e) { console.warn('采集标签失败:', e); } } // 背景图采集 const elementsWithBg = document.querySelectorAll('*'); for (const el of elementsWithBg) { try { const bgStyle = window.getComputedStyle(el).backgroundImage; if (!bgStyle || bgStyle === 'none' || processedUrls.has(bgStyle)) continue; const bgUrls = bgStyle.match(/url\(["']?([^"']+)["']?\)/g); if (!bgUrls) continue; for (const bgUrl of bgUrls) { try { const match = bgUrl.match(/url\(["']?([^"']+)["']?\)/); if (!match || !match[1]) continue; let imgUrl = match[1]; if (processedUrls.has(imgUrl)) continue; const fullUrl = new URL(imgUrl, window.location.href).href; const originalFormat = getFileExtension(fullUrl).toLowerCase(); let svgContent = ''; if (originalFormat === 'svg') { try { const svgResponse = await fetch(fullUrl); if (svgResponse.ok) { svgContent = await svgResponse.text(); } } catch (svgErr) { console.warn('获取背景SVG内容失败:', svgErr); } } const originalName = `背景图-${el.tagName.toLowerCase()}-${Date.now().toString().slice(-4)}`; const truncatedName = truncateTo4Bytes(originalName); const truncatedFormat = truncateTo4Bytes(originalFormat || CONFIG.defaultImageFormat); const truncatedType = truncateTo4Bytes('背景图'); const imgInfo = { id: `bg-${Date.now()}-${Math.random().toString(36).slice(2)}`, url: fullUrl, name: truncatedName, format: truncatedFormat, width: '背景图', height: '背景图', type: truncatedType, preview: fullUrl, originalName: originalName, originalFormat: originalFormat, originalType: '背景图', svgContent: svgContent }; processedUrls.add(imgUrl); imageItems.push(imgInfo); } catch (e) { console.warn('采集背景图失败:', e); } } } catch (e) { console.warn('处理背景图样式失败:', e); } } // CSS图片采集 for (const imgUrl of cssImageUrls) { try { if (!imgUrl || processedUrls.has(imgUrl)) continue; const fullUrl = new URL(imgUrl, window.location.href).href; const originalFormat = getFileExtension(fullUrl).toLowerCase(); let svgContent = ''; if (originalFormat === 'svg') { try { const svgResponse = await fetch(fullUrl); if (svgResponse.ok) { svgContent = await svgResponse.text(); } } catch (svgErr) { console.warn('获取CSS SVG内容失败:', svgErr); } } const originalName = `CSS图片-${Date.now().toString().slice(-4)}`; const truncatedName = truncateTo4Bytes(originalName); const truncatedFormat = truncateTo4Bytes(originalFormat || CONFIG.defaultImageFormat); const truncatedType = truncateTo4Bytes('CSS图片'); const imgInfo = { id: `css-${Date.now()}-${Math.random().toString(36).slice(2)}`, url: fullUrl, name: truncatedName, format: truncatedFormat, width: 'CSS引用', height: 'CSS引用', type: truncatedType, preview: fullUrl, originalName: originalName, originalFormat: originalFormat, originalType: 'CSS图片', svgContent: svgContent }; processedUrls.add(imgUrl); imageItems.push(imgInfo); } catch (e) { console.warn('采集CSS图片失败:', e); } } // SVG标签采集 const svgElements = document.querySelectorAll('svg'); for (const svg of svgElements) { try { const svgId = `svg-${Date.now()}-${Math.random().toString(36).slice(2)}`; const svgContent = svg.outerHTML; const processedSvgContent = processSVGForPreview(svgContent); const svgUrl = URL.createObjectURL(new Blob([svgContent], { type: 'image/svg+xml' })); blobUrls.push(svgUrl); const originalName = `SVG图片-${Date.now().toString().slice(-4)}`; const truncatedName = truncateTo4Bytes(originalName); const truncatedFormat = truncateTo4Bytes('svg'); const truncatedType = truncateTo4Bytes('SVG标签'); const imgInfo = { id: svgId, url: svgUrl, name: truncatedName, format: truncatedFormat, width: svg.naturalWidth || svg.width.baseVal.value || '自适应', height: svg.naturalHeight || svg.height.baseVal.value || '自适应', type: truncatedType, preview: svgUrl, svgContent: svgContent, originalName: originalName, originalFormat: 'svg', originalType: 'SVG标签' }; imageItems.push(imgInfo); } catch (e) { console.warn('采集SVG标签失败:', e); } } } catch (e) { console.error('图片采集主流程异常:', e); } finally { restoreHiddenElements(); } return imageItems; } function getFileExtension(url) { const path = new URL(url).pathname; const lastPart = path.split('/').pop(); const extMatch = lastPart.match(/\.([^.]+)$/); return extMatch ? extMatch[1].toLowerCase() : ''; } function getImageName(url, altText) { if (altText && altText.trim()) { return altText.trim(); } const path = new URL(url).pathname; const fileName = path.split('/').pop().split('?')[0].split('#')[0]; return fileName || `未知图片-${Date.now().toString().slice(-6)}`; } async function showImageList() { const modal = document.getElementById('svgSnifferModal'); const svgList = document.getElementById('svgList'); const imageCountEl = document.getElementById('imageCount'); svgList.innerHTML = '
正在扫描页面图片资源(含CSS/隐藏元素)...
'; modal.style.display = 'flex'; overlay.style.display = 'block'; try { const imageItems = await collectImages(); globalImageItems = imageItems; imageItemCache.clear(); imageCountEl.textContent = imageItems.length; imageItems.forEach(item => { imageItemCache.set(item.id, item); }); if (imageItems.length === 0) { svgList.innerHTML = '
没有找到任何图片资源(已尝试采集隐藏元素和CSS)
'; return; } svgList.innerHTML = ''; imageItems.forEach(item => { const itemElement = document.createElement('div'); itemElement.className = 'svg-item'; const truncatedTitle = item.name; const fullName = item.originalName || truncatedTitle; const fullFormat = item.originalFormat || CONFIG.defaultImageFormat; const fullType = item.originalType || item.type; // 显示补全后的文件名(用于tooltip提示) const completedFileName = completeImageSuffix(fullName, fullFormat); let previewHtml = ''; if (item.format === 'svg' && item.svgContent) { previewHtml = item.svgContent; } else { previewHtml = `${fullName}`; } itemElement.innerHTML = `
${previewHtml}
${truncatedTitle}
格式: ${item.format} 类型: ${item.type}
`; svgList.appendChild(itemElement); const itemDownloadBtn = itemElement.querySelector('.item-download-btn'); itemDownloadBtn.addEventListener('click', (e) => { e.stopPropagation(); const imgId = e.currentTarget.dataset.imgId; const imgItem = imageItemCache.get(imgId); if (imgItem) { downloadImage(imgItem, imgItem.originalName, imgItem.originalFormat); } }); }); setupModalEvents(); } catch (error) { console.error('扫描图片失败:', error); svgList.innerHTML = '
扫描失败,请刷新页面重试
'; } } // 面板事件绑定 function setupModalEvents() { const modal = document.getElementById('svgSnifferModal'); const closeBtn = modal.querySelector('.close-btn'); const overlay = document.querySelector('.overlay'); const selectAllCheckbox = document.getElementById('selectAll'); const batchDownloadBtn = document.getElementById('batchDownloadBtn'); const copyBtn = modal.querySelector('.copy-btn'); closeBtn.addEventListener('click', () => { modal.style.display = 'none'; overlay.style.display = 'none'; cleanupBlobUrls(); }); overlay.addEventListener('click', () => { modal.style.display = 'none'; overlay.style.display = 'none'; cleanupBlobUrls(); }); selectAllCheckbox.addEventListener('change', (e) => { const checkboxes = modal.querySelectorAll('.svg-checkbox'); checkboxes.forEach(checkbox => { checkbox.checked = e.target.checked; }); }); batchDownloadBtn.addEventListener('click', () => { const checkboxes = modal.querySelectorAll('.svg-checkbox:checked'); const selectedIds = Array.from(checkboxes).map(cb => cb.dataset.id); const selectedItems = selectedIds.map(id => imageItemCache.get(id)).filter(Boolean); if (selectedItems.length === 0) { alert('请至少选择一张图片'); return; } if (selectedItems.length === 1) { const item = selectedItems[0]; downloadImage(item, item.originalName, item.originalFormat); } else { downloadMultipleImages(selectedItems); } }); copyBtn.addEventListener('click', () => { const checkboxes = modal.querySelectorAll('.svg-checkbox:checked'); const selectedIds = Array.from(checkboxes).map(cb => cb.dataset.id); const selectedItems = selectedIds.map(id => imageItemCache.get(id)).filter(Boolean); if (selectedItems.length === 0) { alert('请至少选择一张图片'); return; } const urls = selectedItems.map(item => { const completedName = completeImageSuffix(item.originalName || item.name, item.originalFormat); return `${completedName}: ${item.url}`; }).join('\n'); GM_setClipboard(urls, 'text'); const notification = document.querySelector('.copy-notification'); notification.textContent = `已复制 ${selectedItems.length} 个图片链接到剪贴板`; notification.style.opacity = '1'; setTimeout(() => { notification.style.opacity = '0'; }, 2000); }); } // 下载函数(含SVG修复+后缀补全) function downloadImage(imgItem, originalName, originalFormat) { // 1. 处理文件名:补全后缀 const baseName = originalName || imgItem.name; const completedFileName = completeImageSuffix(baseName, originalFormat); // 2. SVG格式特殊处理 if (completedFileName.endsWith('.svg') && imgItem.svgContent) { try { const svgContent = `${imgItem.svgContent}`; const blob = new Blob([svgContent], { type: 'image/svg+xml;charset=utf-8' }); saveAs(blob, completedFileName); showNotification(`下载成功: ${completedFileName}`, 'success'); return; } catch (svgErr) { console.warn('SVG专属下载失败,尝试备用方案:', svgErr); } } // 3. 其他格式下载(含补全后缀的文件名) const mimeType = completedFileName.endsWith('.svg') ? 'image/svg+xml' : `image/${completedFileName.split('.').pop().toLowerCase()}`; GM_download({ url: imgItem.url, name: completedFileName, mimetype: mimeType, onload: () => { showNotification(`下载成功: ${completedFileName}`, 'success'); }, onerror: (e) => { console.error('下载失败:', e); showNotification(`下载失败: ${completedFileName}`, 'error'); } }); } // 批量下载函数(含后缀补全) async function downloadMultipleImages(selectedItems) { const zip = new JSZip(); let downloadedCount = 0; const totalCount = selectedItems.length; for (const imgItem of selectedItems) { try { const baseName = imgItem.originalName || imgItem.name; const originalFormat = imgItem.originalFormat || ''; // 补全后缀的文件名 const completedFileName = completeImageSuffix(baseName, originalFormat); // SVG特殊处理 if (completedFileName.endsWith('.svg') && imgItem.svgContent) { const svgContent = `${imgItem.svgContent}`; zip.file(completedFileName, svgContent); downloadedCount++; continue; } // 其他格式下载 const response = await fetch(imgItem.url); if (!response.ok) throw new Error(`HTTP ${response.status}`); const blob = await response.blob(); zip.file(completedFileName, blob); downloadedCount++; } catch (error) { const errBaseName = imgItem.originalName || imgItem.name; const errFileName = completeImageSuffix(`${errBaseName}_加载失败`, 'txt'); zip.file(errFileName, `图片加载失败: ${imgItem.url}\n错误原因: ${error.message}`); console.error(`下载失败 ${errBaseName}:`, error); } } if (downloadedCount === 0) { alert('所有图片下载失败'); return; } try { const content = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 6 } }); const zipFileName = `网页图片_${location.hostname}_${new Date().toISOString().slice(0, 10)}.zip`; saveAs(content, zipFileName); showNotification(`批量下载成功: ${zipFileName}(共${downloadedCount}/${totalCount}个)`, 'success'); if (downloadedCount < totalCount) { alert(`部分图片下载失败,成功下载 ${downloadedCount}/${totalCount} 个资源`); } } catch (error) { console.error('创建ZIP失败:', error); showNotification('创建ZIP文件失败', 'error'); alert('创建ZIP文件失败'); } } function cleanupBlobUrls() { blobUrls.forEach(url => { try { URL.revokeObjectURL(url); } catch (e) { console.warn('清理Blob URL失败:', e); } }); blobUrls = []; } function showNotification(message, type = 'info') { const colors = { info: '#3498db', success: '#27ae60', warning: '#f39c12', error: '#e74c3c' }; copyNotification.textContent = message; copyNotification.style.backgroundColor = colors[type] || colors.info; copyNotification.style.opacity = '1'; setTimeout(() => { copyNotification.style.opacity = '0'; }, 3000); } initRadarButton(); })();