NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name 小庄智能管家
// @namespace http://tampermonkey.net/
// @version 0.1
// @description 一个强大的前端开发辅助工具,提供元素查看器、CSS选择器生成、API请求监控等功能,帮助开发者提高工作效率
// @author 小庄
// @match *://*/*
// @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iIzRjYWY1MCIgZD0iTTExLjksMTIuOTdjLTAuNDEtMC40MS0xLjA4LTAuNDEtMS40OSwwbC0yLjk4LDIuOThjLTAuNDEsMC40MS0wLjQxLDEuMDgsMCwxLjQ5YzAuNDEsMC40MSwxLjA4LDAuNDEsMS40OSwwTDExLjksMTQuNDVjMC40MS0wLjQxLDAuNDEtMS4wOCwwLTEuNDh6Ii8+PHBhdGggZmlsbD0iIzFiNWUyMCIgZD0iTTExLjE0LDcuMzdjNS44LDAuNDgsOS45OCw1LjUyLDkuNiwxMS4yMUMxMC44NiwxOC4yLDguMTcsMTUuNTIsNy43OCw5LjgzQzYuNzksMTIuMTksNi4xNiwxNC44Niw2LDE3LjYgYy00LjA0LTEuNzctNi4yNS02LjI3LTQuOTgtMTAuOThjMS4yOC00LjcxLDYuMTYtNy4xLDEwLjEyLTQuOThWNy4zN3oiLz48L3N2Zz4=
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant unsafeWindow
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// 样式定义
const styles = `
.dev-helper-panel {
position: fixed;
right: 0;
top: 0;
width: 300px;
height: 100vh;
background: #212121;
color: #eee;
z-index: 999999;
font-family: Arial, sans-serif;
font-size: 14px;
box-shadow: -2px 0 5px rgba(0,0,0,0.3);
transition: transform 0.3s;
transform: translateX(300px);
overflow: auto;
}
.dev-helper-panel.active {
transform: translateX(0);
}
.dev-helper-toggle {
position: fixed;
top: 120px;
right: 0;
background: #4caf50;
color: white;
padding: 10px;
cursor: pointer;
z-index: 999999;
border-radius: 5px 0 0 5px;
box-shadow: -2px 0 5px rgba(0,0,0,0.2);
}
.dev-helper-header {
padding: 10px;
background: #4caf50;
color: white;
text-align: center;
}
.dev-helper-container {
padding: 10px;
}
.dev-helper-tabs {
display: flex;
border-bottom: 1px solid #444;
}
.dev-helper-tab {
padding: 8px 12px;
cursor: pointer;
border-bottom: 2px solid transparent;
}
.dev-helper-tab.active {
border-bottom: 2px solid #4caf50;
color: #4caf50;
}
.dev-helper-tab-content {
padding: 10px 0;
display: none;
}
.dev-helper-tab-content.active {
display: block;
}
.dev-helper-button {
background: #4caf50;
border: none;
color: white;
padding: 5px 10px;
margin: 5px 0;
cursor: pointer;
border-radius: 3px;
}
.dev-helper-table {
width: 100%;
border-collapse: collapse;
}
.dev-helper-table th, .dev-helper-table td {
padding: 5px;
border-bottom: 1px solid #444;
text-align: left;
}
.dev-helper-label {
display: block;
margin: 5px 0;
}
.dev-helper-input {
width: 100%;
padding: 5px;
margin-bottom: 10px;
background: #333;
color: #fff;
border: 1px solid #555;
border-radius: 3px;
}
.dev-helper-select {
width: 100%;
padding: 5px;
margin-bottom: 10px;
background: #333;
color: #fff;
border: 1px solid #555;
border-radius: 3px;
}
.element-highlight {
outline: 2px dashed #f44336 !important;
background-color: rgba(244, 67, 54, 0.1) !important;
}
.request-item {
margin-bottom: 8px;
padding: 8px;
background: #333;
border-radius: 3px;
}
.request-method {
display: inline-block;
padding: 2px 5px;
margin-right: 5px;
border-radius: 3px;
}
.request-method.get {
background: #3f51b5;
}
.request-method.post {
background: #4caf50;
}
.request-method.put {
background: #ff9800;
}
.request-method.delete {
background: #f44336;
}
.request-url {
word-break: break-all;
}
.collapsible-content {
margin-top: 5px;
display: none;
}
.collapsible-trigger {
cursor: pointer;
text-decoration: underline;
color: #4caf50;
}
.color-preview {
display: inline-block;
width: 16px;
height: 16px;
margin-right: 5px;
border: 1px solid #555;
vertical-align: middle;
}
`;
// 添加样式
GM_addStyle(styles);
// 创建UI
function createUI() {
const panel = document.createElement('div');
panel.className = 'dev-helper-panel';
panel.innerHTML = `
<div class="dev-helper-header">
<h3>小庄智能管家</h3>
</div>
<div class="dev-helper-container">
<div class="dev-helper-tabs">
<div class="dev-helper-tab active" data-tab="element-inspector">元素查看器</div>
<div class="dev-helper-tab" data-tab="css-tools">CSS工具</div>
<div class="dev-helper-tab" data-tab="api-monitor">API监控</div>
</div>
<div class="dev-helper-tab-content active" data-tab="element-inspector">
<p>单击下方按钮,然后点击页面上的元素以查看其属性</p>
<button class="dev-helper-button" id="start-inspect">开始查看元素</button>
<div id="element-info">
<h4>元素信息</h4>
<div id="element-details"></div>
</div>
</div>
<div class="dev-helper-tab-content" data-tab="css-tools">
<div>
<h4>CSS选择器生成器</h4>
<button class="dev-helper-button" id="generate-selector">选择元素生成选择器</button>
<div id="selector-result"></div>
</div>
<div style="margin-top: 20px;">
<h4>CSS颜色转换</h4>
<div>
<label class="dev-helper-label">输入颜色值</label>
<input type="text" class="dev-helper-input" id="color-input" placeholder="#FF0000 或 rgb(255,0,0)">
<button class="dev-helper-button" id="convert-color">转换</button>
<div id="color-result"></div>
</div>
</div>
</div>
<div class="dev-helper-tab-content" data-tab="api-monitor">
<h4>网络请求监控</h4>
<button class="dev-helper-button" id="clear-requests">清除请求</button>
<div id="requests-list"></div>
</div>
</div>
`;
document.body.appendChild(panel);
// 创建切换按钮
const toggle = document.createElement('div');
toggle.className = 'dev-helper-toggle';
toggle.textContent = '开发助手';
toggle.addEventListener('click', function () {
panel.classList.toggle('active');
saveState();
});
document.body.appendChild(toggle);
// 初始化标签页切换
initializeTabs();
// 初始化元素检查功能
initializeElementInspector();
// 初始化CSS工具
initializeCssTools();
// 初始化API监控
initializeApiMonitor();
// 加载已保存的状态
loadState();
}
// 初始化标签页切换
function initializeTabs() {
const tabs = document.querySelectorAll('.dev-helper-tab');
tabs.forEach(tab => {
tab.addEventListener('click', function () {
// 移除所有活动标签
document.querySelectorAll('.dev-helper-tab').forEach(t => {
t.classList.remove('active');
});
document.querySelectorAll('.dev-helper-tab-content').forEach(c => {
c.classList.remove('active');
});
// 激活当前标签
this.classList.add('active');
const tabName = this.getAttribute('data-tab');
document.querySelector(`.dev-helper-tab-content[data-tab="${tabName}"]`).classList.add('active');
// 保存状态
saveState();
});
});
}
// 初始化元素检查功能
function initializeElementInspector() {
let isInspecting = false;
let highlightedElement = null;
document.getElementById('start-inspect').addEventListener('click', function () {
isInspecting = !isInspecting;
this.textContent = isInspecting ? '停止查看元素' : '开始查看元素';
if (!isInspecting && highlightedElement) {
highlightedElement.classList.remove('element-highlight');
highlightedElement = null;
}
});
document.addEventListener('mouseover', function (e) {
if (!isInspecting) return;
if (highlightedElement) {
highlightedElement.classList.remove('element-highlight');
}
if (!e.target.closest('.dev-helper-panel') && !e.target.closest('.dev-helper-toggle')) {
highlightedElement = e.target;
highlightedElement.classList.add('element-highlight');
}
}, true);
document.addEventListener('click', function (e) {
if (!isInspecting) return;
if (!e.target.closest('.dev-helper-panel') && !e.target.closest('.dev-helper-toggle')) {
e.preventDefault();
e.stopPropagation();
displayElementInfo(e.target);
// 停止检查
isInspecting = false;
document.getElementById('start-inspect').textContent = '开始查看元素';
}
}, true);
}
// 显示元素信息
function displayElementInfo(element) {
const details = document.getElementById('element-details');
// 获取元素标签名称
const tagName = element.tagName.toLowerCase();
// 获取元素ID
const id = element.id ? element.id : '无';
// 获取元素类名
const className = element.className ? element.className : '无';
// 获取元素样式
const computedStyle = window.getComputedStyle(element);
// 获取元素尺寸和位置
const rect = element.getBoundingClientRect();
// 生成唯一CSS选择器
const selector = generateSelector(element);
let html = `
<table class="dev-helper-table">
<tr>
<th>标签名</th>
<td>${tagName}</td>
</tr>
<tr>
<th>ID</th>
<td>${id}</td>
</tr>
<tr>
<th>类名</th>
<td>${className}</td>
</tr>
<tr>
<th>宽度</th>
<td>${Math.round(rect.width)}px</td>
</tr>
<tr>
<th>高度</th>
<td>${Math.round(rect.height)}px</td>
</tr>
<tr>
<th>位置</th>
<td>X: ${Math.round(rect.left)}px, Y: ${Math.round(rect.top)}px</td>
</tr>
<tr>
<th>CSS选择器</th>
<td>${selector}</td>
</tr>
</table>
<h4 style="margin-top: 10px;">计算样式</h4>
<div class="collapsible-trigger" data-target="computed-style">查看计算样式</div>
<div class="collapsible-content" id="computed-style">
<table class="dev-helper-table">
<tr>
<th>颜色</th>
<td><span class="color-preview" style="background-color: ${computedStyle.color}"></span>${computedStyle.color}</td>
</tr>
<tr>
<th>背景色</th>
<td><span class="color-preview" style="background-color: ${computedStyle.backgroundColor}"></span>${computedStyle.backgroundColor}</td>
</tr>
<tr>
<th>字体</th>
<td>${computedStyle.fontFamily}</td>
</tr>
<tr>
<th>字体大小</th>
<td>${computedStyle.fontSize}</td>
</tr>
<tr>
<th>行高</th>
<td>${computedStyle.lineHeight}</td>
</tr>
<tr>
<th>内边距</th>
<td>${computedStyle.padding}</td>
</tr>
<tr>
<th>外边距</th>
<td>${computedStyle.margin}</td>
</tr>
<tr>
<th>盒模型</th>
<td>${computedStyle.boxSizing}</td>
</tr>
<tr>
<th>显示方式</th>
<td>${computedStyle.display}</td>
</tr>
<tr>
<th>定位</th>
<td>${computedStyle.position}</td>
</tr>
</table>
</div>
<h4 style="margin-top: 10px;">DOM属性</h4>
<div class="collapsible-trigger" data-target="element-attributes">查看DOM属性</div>
<div class="collapsible-content" id="element-attributes">
<table class="dev-helper-table">
`;
// 添加元素的属性
for (let i = 0; i < element.attributes.length; i++) {
const attr = element.attributes[i];
html += `
<tr>
<th>${attr.name}</th>
<td>${attr.value}</td>
</tr>
`;
}
html += `
</table>
</div>
`;
details.innerHTML = html;
// 初始化折叠面板
initializeCollapsibles();
}
// 初始化折叠面板
function initializeCollapsibles() {
document.querySelectorAll('.collapsible-trigger').forEach(trigger => {
trigger.addEventListener('click', function () {
const targetId = this.getAttribute('data-target');
const target = document.getElementById(targetId);
if (target.style.display === 'block') {
target.style.display = 'none';
this.textContent = this.textContent.replace('隐藏', '查看');
} else {
target.style.display = 'block';
this.textContent = this.textContent.replace('查看', '隐藏');
}
});
});
}
// 生成CSS选择器
function generateSelector(element) {
let path = [];
let current = element;
while (current && current !== document && current !== document.documentElement) {
// 尝试使用ID
if (current.id) {
path.unshift('#' + current.id);
break; // ID应该是唯一的,所以我们可以在这里停止
} else {
// 使用标签名称和类(如果存在)
let selector = current.tagName.toLowerCase();
if (current.className) {
const classes = current.className.split(' ').filter(c => c);
if (classes.length > 0) {
selector += '.' + classes.join('.');
}
}
// 如果有相同类型的兄弟元素,添加索引
let sibling = current;
let index = 1;
while (sibling = sibling.previousElementSibling) {
if (sibling.tagName === current.tagName) {
index++;
}
}
if (index > 1) {
selector += `:nth-of-type(${index})`;
}
path.unshift(selector);
current = current.parentElement;
}
}
return path.join(' > ');
}
// 初始化CSS工具
function initializeCssTools() {
let selectedElement = null;
// CSS选择器生成
document.getElementById('generate-selector').addEventListener('click', function () {
this.textContent = '请选择一个元素...';
const clickHandler = function (e) {
if (!e.target.closest('.dev-helper-panel') && !e.target.closest('.dev-helper-toggle')) {
e.preventDefault();
e.stopPropagation();
selectedElement = e.target;
const selector = generateSelector(selectedElement);
document.getElementById('selector-result').innerHTML = `
<div style="margin-top: 10px;">
<label class="dev-helper-label">生成的选择器:</label>
<input type="text" class="dev-helper-input" value="${selector}" readonly>
<button class="dev-helper-button" id="copy-selector">复制</button>
</div>
`;
document.getElementById('copy-selector').addEventListener('click', function () {
const selectorText = this.previousElementSibling.value;
navigator.clipboard.writeText(selectorText).then(() => {
this.textContent = '已复制!';
setTimeout(() => {
this.textContent = '复制';
}, 2000);
});
});
document.getElementById('generate-selector').textContent = '选择元素生成选择器';
document.removeEventListener('click', clickHandler, true);
}
};
document.addEventListener('click', clickHandler, true);
});
// 颜色转换
document.getElementById('convert-color').addEventListener('click', function () {
const colorInput = document.getElementById('color-input').value.trim();
if (!colorInput) {
document.getElementById('color-result').innerHTML = `<p style="color: #f44336;">请输入颜色值</p>`;
return;
}
try {
// 创建一个临时元素来计算颜色
const tempElement = document.createElement('div');
tempElement.style.color = colorInput;
document.body.appendChild(tempElement);
// 获取计算后的颜色
const computedColor = window.getComputedStyle(tempElement).color;
document.body.removeChild(tempElement);
// 将RGB转换为HEX
const rgbToHex = function (rgb) {
if (!rgb) return '';
// 检查是否已经是十六进制格式
if (rgb.indexOf('#') === 0) {
return rgb;
}
// 提取RGB值
const rgbArr = rgb.match(/\d+/g);
if (!rgbArr || rgbArr.length < 3) return '';
const r = parseInt(rgbArr[0]);
const g = parseInt(rgbArr[1]);
const b = parseInt(rgbArr[2]);
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
};
// 将RGB转换为HSL
const rgbToHsl = function (rgb) {
if (!rgb) return '';
// 提取RGB值
const rgbArr = rgb.match(/\d+/g);
if (!rgbArr || rgbArr.length < 3) return '';
let r = parseInt(rgbArr[0]) / 255;
let g = parseInt(rgbArr[1]) / 255;
let b = parseInt(rgbArr[2]) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0; // 灰色
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h = Math.round(h * 60);
}
s = Math.round(s * 100);
l = Math.round(l * 100);
return `hsl(${h}, ${s}%, ${l}%)`;
};
const hexColor = rgbToHex(computedColor);
const hslColor = rgbToHsl(computedColor);
document.getElementById('color-result').innerHTML = `
<div style="margin-top: 10px;">
<div>
<span class="color-preview" style="background-color: ${computedColor}"></span>
<span>预览</span>
</div>
<table class="dev-helper-table">
<tr>
<th>RGB</th>
<td>${computedColor}</td>
</tr>
<tr>
<th>HEX</th>
<td>${hexColor}</td>
</tr>
<tr>
<th>HSL</th>
<td>${hslColor}</td>
</tr>
</table>
</div>
`;
} catch (error) {
document.getElementById('color-result').innerHTML = `<p style="color: #f44336;">无效的颜色值</p>`;
}
});
}
// 初始化API监控
function initializeApiMonitor() {
const requests = [];
const maxRequests = 50;
// 清除请求列表
document.getElementById('clear-requests').addEventListener('click', function () {
requests.length = 0;
updateRequestsList();
});
// 拦截XMLHttpRequest
const originalXHR = unsafeWindow.XMLHttpRequest;
unsafeWindow.XMLHttpRequest = function () {
const xhr = new originalXHR();
const originalOpen = xhr.open;
const originalSend = xhr.send;
xhr.open = function () {
this._method = arguments[0];
this._url = arguments[1];
return originalOpen.apply(this, arguments);
};
xhr.send = function () {
this._startTime = new Date();
this._data = arguments[0] || null;
this.addEventListener('load', function () {
try {
const duration = new Date() - this._startTime;
const requestInfo = {
type: 'xhr',
method: this._method,
url: this._url,
status: this.status,
statusText: this.statusText,
duration: duration,
requestData: this._data,
responseData: this.responseText,
timestamp: new Date()
};
addRequest(requestInfo);
} catch (e) {
console.error('Error monitoring XHR:', e);
}
});
return originalSend.apply(this, arguments);
};
return xhr;
};
// 拦截Fetch
const originalFetch = unsafeWindow.fetch;
unsafeWindow.fetch = function () {
const startTime = new Date();
const url = arguments[0] instanceof Request ? arguments[0].url : arguments[0];
const options = arguments[1] || {};
const method = options.method || 'GET';
const data = options.body || null;
return originalFetch.apply(this, arguments)
.then(response => {
const duration = new Date() - startTime;
// 克隆响应以避免消耗流
const clonedResponse = response.clone();
clonedResponse.text().then(responseText => {
const requestInfo = {
type: 'fetch',
method: method,
url: url,
status: response.status,
statusText: response.statusText,
duration: duration,
requestData: data,
responseData: responseText,
timestamp: new Date()
};
addRequest(requestInfo);
});
return response;
})
.catch(error => {
const duration = new Date() - startTime;
const requestInfo = {
type: 'fetch',
method: method,
url: url,
status: 0,
statusText: 'Error: ' + error.message,
duration: duration,
requestData: data,
responseData: null,
timestamp: new Date()
};
addRequest(requestInfo);
throw error;
});
};
// 添加请求到列表
function addRequest(requestInfo) {
requests.unshift(requestInfo);
// 限制请求数量
if (requests.length > maxRequests) {
requests.pop();
}
updateRequestsList();
}
// 更新请求列表UI
function updateRequestsList() {
const requestsList = document.getElementById('requests-list');
if (requests.length === 0) {
requestsList.innerHTML = '<p>尚无请求数据</p>';
return;
}
let html = '';
requests.forEach((request, index) => {
const statusClass = request.status >= 200 && request.status < 300 ? 'success' : 'error';
const methodClass = request.method.toLowerCase();
html += `
<div class="request-item">
<div>
<span class="request-method ${methodClass}">${request.method}</span>
<span class="request-status ${statusClass}">${request.status}</span>
<span>${Math.round(request.duration)}ms</span>
</div>
<div class="request-url">${request.url}</div>
<div class="collapsible-trigger" data-target="request-details-${index}">查看详情</div>
<div class="collapsible-content" id="request-details-${index}">
<table class="dev-helper-table">
<tr>
<th>请求时间</th>
<td>${request.timestamp.toLocaleTimeString()}</td>
</tr>
<tr>
<th>请求类型</th>
<td>${request.type}</td>
</tr>
<tr>
<th>状态文本</th>
<td>${request.statusText}</td>
</tr>
</table>
<div style="margin-top: 10px;">
<div class="collapsible-trigger" data-target="request-data-${index}">查看请求数据</div>
<div class="collapsible-content" id="request-data-${index}">
<pre style="max-height: 200px; overflow: auto; background: #333; padding: 5px; white-space: pre-wrap; word-break: break-all;">${formatData(request.requestData)}</pre>
</div>
</div>
<div style="margin-top: 10px;">
<div class="collapsible-trigger" data-target="response-data-${index}">查看响应数据</div>
<div class="collapsible-content" id="response-data-${index}">
<pre style="max-height: 200px; overflow: auto; background: #333; padding: 5px; white-space: pre-wrap; word-break: break-all;">${formatData(request.responseData)}</pre>
</div>
</div>
</div>
</div>
`;
});
requestsList.innerHTML = html;
// 初始化折叠面板
initializeCollapsibles();
}
// 格式化数据
function formatData(data) {
if (!data) return '无数据';
try {
// 尝试解析并美化JSON
const parsed = JSON.parse(data);
return JSON.stringify(parsed, null, 2);
} catch (e) {
// 如果不是JSON,则直接返回原始数据
return data;
}
}
}
// 保存面板状态
function saveState() {
const panel = document.querySelector('.dev-helper-panel');
const isActive = panel.classList.contains('active');
const activeTab = document.querySelector('.dev-helper-tab.active');
const activeTabName = activeTab ? activeTab.getAttribute('data-tab') : 'element-inspector';
GM_setValue('devHelperPanelState', {
isActive,
activeTab: activeTabName
});
}
// 加载面板状态
function loadState() {
const savedState = GM_getValue('devHelperPanelState');
if (savedState) {
const panel = document.querySelector('.dev-helper-panel');
if (savedState.isActive) {
panel.classList.add('active');
}
if (savedState.activeTab) {
// 激活保存的标签
document.querySelectorAll('.dev-helper-tab').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.dev-helper-tab-content').forEach(content => {
content.classList.remove('active');
});
const tab = document.querySelector(`.dev-helper-tab[data-tab="${savedState.activeTab}"]`);
const content = document.querySelector(`.dev-helper-tab-content[data-tab="${savedState.activeTab}"]`);
if (tab && content) {
tab.classList.add('active');
content.classList.add('active');
}
}
}
}
// 注册菜单命令
GM_registerMenuCommand('显示/隐藏开发助手', function () {
const panel = document.querySelector('.dev-helper-panel');
panel.classList.toggle('active');
saveState();
});
// 当页面加载完成后创建UI
window.addEventListener('load', function () {
// 检查页面上是否已有开发助手面板
if (!document.querySelector('.dev-helper-panel')) {
createUI();
}
});
// 如果文档已经加载完成,则立即创建UI
if (document.readyState === 'complete' || document.readyState === 'interactive') {
createUI();
}
})();