// ==UserScript==
// @name SVG嗅探器
// @namespace http://tampermonkey.net/
// @version 1.1
// @description 嗅探页面中的SVG资源
// @author YourName
// @match *://*/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_setClipboard
// @grant GM_notification
// @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/149/149071.png
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 配置参数
const CONFIG = {
buttonSize: 30,
activeColor: '#3498db',
hoverColor: '#2980b9',
zIndex: 99999,
positionOffset: 25,
touchDelay: 300
};
// 添加主样式
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, #3498db, #2c3e50);
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 {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
max-width: 800px;
max-height: 80vh;
background: white;
z-index: 10000;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
overflow: hidden;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.modal-header {
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
padding: 18px 25px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.modal-header h2 {
margin: 0;
font-size: 1.4rem;
font-weight: 600;
}
.close-btn {
background: none;
border: none;
color: white;
font-size: 1.8rem;
cursor: pointer;
width: 36px;
height: 36px;
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 {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 25px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
.select-all-control {
display: flex;
align-items: center;
gap: 10px;
font-size: 1rem;
}
.action-buttons {
display: flex;
gap: 12px;
}
.action-btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 0.95rem;
transition: all 0.2s;
}
.download-btn {
background: #27ae60;
color: white;
}
.download-btn:hover {
background: #219653;
transform: translateY(-2px);
}
.copy-btn {
background: #2980b9;
color: white;
}
.copy-btn:hover {
background: #2573a7;
transform: translateY(-2px);
}
.modal-content {
padding: 20px;
overflow-y: auto;
max-height: 60vh;
}
.svg-item {
display: flex;
align-items: center;
padding: 15px;
border-bottom: 1px solid #eee;
transition: background 0.2s;
}
.svg-item:hover {
background-color: #f8fafc;
}
.svg-checkbox {
margin-right: 20px;
width: 20px;
height: 20px;
cursor: pointer;
}
.svg-preview {
width: 50px;
height: 50px;
margin-right: 20px;
border: 1px solid #e0e0e0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
background: white;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
}
.svg-preview svg {
max-width: 100%;
max-height: 100%;
}
.svg-name {
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 1.05rem;
color: #2c3e50;
}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: none;
}
.loading {
text-align: center;
padding: 30px;
font-size: 1.2rem;
color: #666;
}
.copy-notification {
position: fixed;
top: 30px;
left: 50%;
transform: translateX(-50%);
background-color: #27ae60;
color: white;
padding: 12px 25px;
border-radius: 6px;
z-index: 100000;
opacity: 0;
transition: opacity 0.5s;
pointer-events: none;
white-space: nowrap;
font-weight: 500;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
@keyframes radar-scan {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(52, 152, 219, 0.6);
}
70% {
box-shadow: 0 0 0 12px rgba(52, 152, 219, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(52, 152, 219, 0);
}
}
`);
// 创建按钮容器
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 = `
<div class="radar-icon">
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="white">
<path d="M512 625.8c-63 0-113.8-51-113.8-113.8s51-113.8 113.8-113.8 113.8 51 113.8 113.8-50.8 113.8-113.8 113.8z m0-165.6c-28.6 0-51.8 23.2-51.8 51.8 0 28.6 23.2 51.8 51.8 51.8 28.6 0 51.8-23.2 51.8-51.8 0-28.6-23.2-51.8-51.8-51.8zM843.2 791.4c-6.6 0-12.8-2-18.6-6.2-13.6-10.4-16.6-29.8-6.2-43.4 50-66.6 76.6-146.2 76.6-229.8s-26.4-163-76.6-229.8c-10.4-13.6-7.4-33.2 6.2-43.4 13.6-10.4 33.2-7.4 43.4 6.2 58.4 77.4 89 169.8 89 267s-30.6 189.6-89 267c-6.2 8.2-15.4 12.4-24.8 12.4zM180.8 791.4c-9.6 0-18.6-4.2-24.8-12.4-58.4-77.4-89-169.8-89-267S97.6 322.4 156 245c10.4-13.6 29.8-16.6 43.4-6.2 13.6 10.4 16.6 29.8 6.2 43.4-50 66.6-76.6 146.2-76.6 229.8s26.4 163 76.6 229.8c10.4 13.6 7.4 33.2-6.2 43.4-5.4 4.2-12 6.2-18.6 6.2zM710.8 692c-6.6 0-12.8-2-18.6-6.2-13.6-10.4-16.6-29.8-6.2-43.4 28.6-37.6 43.4-82.8 43.4-130.4s-15-92.8-43.4-130.4c-10.4-13.6-7.4-33.2 6.2-43.4 13.6-10.4 33.2-7.4 43.4 6.2 36.4 48.8 55.8 106.8 55.8 167.6s-19.4 119.2-55.8 167.6c-6.2 8.4-15.4 12.4-24.8 12.4zM313.4 692c-9.6 0-18.6-4.2-24.8-12.4-36.4-48.8-55.8-106.8-55.8-167.6s19.4-119.2 55.8-167.6c10.4-13.6 29.8-16.6 43.4-6.2 13.6 10.4 16.6 29.8 6.2 43.4-28.6 37.6-43.4 82.8-43.4 130.4s15 92.8 43.4 130.4c10.4 13.6 7.4 33.2-6.2 43.4-5.4 4.2-12 6.2-18.6 6.2z"></path>
</svg>
</div>
`;
radarContainer.appendChild(radarButton);
document.body.appendChild(radarContainer);
// 创建SVG嗅探模态框
const svgModal = document.createElement('div');
svgModal.id = 'svgSnifferModal';
svgModal.innerHTML = `
<div class="modal-header">
<h2>SVG资源列表</h2>
<button class="close-btn">×</button>
</div>
<div class="action-bar">
<div class="select-all-control">
<input type="checkbox" id="selectAll">
<label for="selectAll">全选</label>
</div>
<div class="action-buttons">
<button class="action-btn download-btn">下载选中</button>
<button class="action-btn copy-btn">复制SVG</button>
</div>
</div>
<div class="modal-content" id="svgList">
<div class="loading">正在扫描页面SVG资源...</div>
</div>
`;
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 globalSvgItems = [];
let isDragging = false;
let startX, startY, startLeft, startTop;
let dragStartTime = 0;
let touchTimer = null;
// 初始化按钮
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) {
showSVGList();
}
});
}
// 开始拖拽
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;
// 如果使用right定位,转换为left定位
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) {
showSVGList();
}
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
});
}
// 收集SVG函数
function collectSVGs() {
const svgItems = [];
// 收集页面中的SVG元素
const svgElements = document.querySelectorAll('svg');
svgElements.forEach((svg, index) => {
// 尝试获取有意义的名称
let name = `SVG ${index + 1}`;
let parent = svg.parentElement;
while (parent) {
if (parent.querySelector('h1, h2, h3, h4, h5, h6, [class*="title"], [class*="name"]')) {
const nameElement = parent.querySelector('h1, h2, h3, h4, h5, h6, [class*="title"], [class*="name"]');
if (nameElement) {
name = nameElement.textContent.trim() || name;
break;
}
}
parent = parent.parentElement;
}
// 克隆SVG元素以避免修改原始元素
const clonedSvg = svg.cloneNode(true);
// 移除干扰属性
clonedSvg.removeAttribute('onclick');
clonedSvg.removeAttribute('onmouseover');
clonedSvg.removeAttribute('onmouseout');
svgItems.push({
name: name,
svg: clonedSvg.outerHTML,
id: `svg-${index}-${Date.now()}`
});
});
return svgItems;
}
// 显示SVG列表
function showSVGList() {
const modal = document.getElementById('svgSnifferModal');
const svgList = document.getElementById('svgList');
svgList.innerHTML = '<div class="loading">正在扫描页面SVG资源...</div>';
modal.style.display = 'block';
overlay.style.display = 'block';
// 使用setTimeout让UI有机会更新
setTimeout(() => {
try {
const svgItems = collectSVGs();
globalSvgItems = svgItems; // 存储到全局变量
if (svgItems.length === 0) {
svgList.innerHTML = '<div class="loading">没有找到SVG资源</div>';
return;
}
svgList.innerHTML = '';
svgItems.forEach(item => {
const itemElement = document.createElement('div');
itemElement.className = 'svg-item';
itemElement.innerHTML = `
<input type="checkbox" class="svg-checkbox" data-id="${item.id}" checked>
<div class="svg-preview">${item.svg}</div>
<div class="svg-name" title="${item.name}">${item.name}</div>
`;
svgList.appendChild(itemElement);
});
} catch (error) {
console.error('SVG扫描错误:', error);
svgList.innerHTML = `<div class="loading">错误: ${error.message}</div>`;
}
}, 300);
}
// 下载选中的SVG
function downloadSelected() {
const checkboxes = document.querySelectorAll('.svg-checkbox:checked');
if (checkboxes.length === 0) {
GM_notification({
text: '请至少选择一个SVG!',
title: 'SVG嗅探器',
silent: true
});
return;
}
const zip = new JSZip();
checkboxes.forEach(checkbox => {
const id = checkbox.dataset.id;
const item = globalSvgItems.find(i => i.id === id);
if (item) {
// 清理文件名
const cleanName = item.name.replace(/[^\w\u4e00-\u9fa5]/g, '_').substring(0, 50);
zip.file(`${cleanName}.svg`, item.svg);
}
});
zip.generateAsync({type: 'blob'}).then(content => {
saveAs(content, 'svg_collection.zip');
});
}
// 复制选中的SVG代码
function copySelected() {
const checkboxes = document.querySelectorAll('.svg-checkbox:checked');
if (checkboxes.length === 0) {
GM_notification({
text: '请至少选择一个SVG!',
title: 'SVG嗅探器',
silent: true
});
return;
}
// 构建SVG代码字符串
let combinedCode = '';
checkboxes.forEach(checkbox => {
const id = checkbox.dataset.id;
const item = globalSvgItems.find(i => i.id === id);
if (item) {
combinedCode += `${item.svg}\n\n`;
}
});
// 尝试复制到剪贴板
try {
GM_setClipboard(combinedCode, 'text');
showCopyNotification(`已复制 ${checkboxes.length} 个SVG代码`);
} catch (e) {
// 回退方案
const textArea = document.createElement('textarea');
textArea.value = combinedCode;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showCopyNotification(`已复制 ${checkboxes.length} 个SVG代码 (使用回退方法)`);
}
}
// 显示复制通知
function showCopyNotification(message) {
copyNotification.textContent = message;
copyNotification.style.opacity = '1';
setTimeout(() => {
copyNotification.style.opacity = '0';
}, 3000);
}
// 设置事件监听器
function setupEventListeners() {
// 关闭模态框
document.querySelector('.close-btn').addEventListener('click', () => {
document.getElementById('svgSnifferModal').style.display = 'none';
overlay.style.display = 'none';
});
// 点击遮罩层关闭模态框
overlay.addEventListener('click', () => {
document.getElementById('svgSnifferModal').style.display = 'none';
overlay.style.display = 'none';
});
// 下载按钮
document.querySelector('.download-btn').addEventListener('click', downloadSelected);
// 复制按钮
document.querySelector('.copy-btn').addEventListener('click', copySelected);
// 全选/取消全选
document.getElementById('selectAll').addEventListener('change', (e) => {
const checkboxes = document.querySelectorAll('.svg-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = e.target.checked;
});
});
}
// 初始化脚本
function init() {
initRadarButton();
setupEventListeners();
// 确保元素已正确添加到DOM
setTimeout(() => {
radarButton.style.display = 'flex';
}, 100);
}
// 启动脚本
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();