// ==UserScript==
// @name 元器件信息提取
// @namespace http://tampermonkey.net/
// @version 1.7.5
// @description 提取表格、flex容器、图片URL、名称、PDF下载链接及存放位置并上传
// @author lzq-hopego
// @match https://item.szlcsc.com/*
// @exclude https://item.szlcsc.com/datasheet/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net
// @grant GM_xmlhttpRequest
// @connect 127.0.0.1
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 创建悬浮框样式
const style = document.createElement('style');
style.textContent = `
.float-container {
position: fixed;
top: 20px;
right: 20px;
background: white;
padding: 15px;
border: 1px solid #ccc;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
z-index: 9999;
width: 300px;
user-select: none;
}
.float-container.collapsed {
width: 120px;
padding: 5px;
transition: all 0.3s ease;
}
.float-container .title {
margin: 0 0 10px 0;
font-size: 16px;
color: #333;
}
.float-container.collapsed .title {
margin: 0;
font-size: 14px;
text-align: center;
}
.float-container .input-group {
margin-bottom: 10px;
}
.float-container .drag-handle {
cursor: move;
text-align: center;
padding: 5px;
background: #f5f5f5;
margin: -15px -15px 10px -15px;
border-bottom: 1px solid #eee;
border-radius: 8px 8px 0 0;
user-select: none;
}
.float-container.collapsed .drag-handle {
margin: -5px -5px 5px -5px;
padding: 3px;
}
.float-container label {
display: block;
margin-bottom: 5px;
font-size: 14px;
color: #666;
}
.float-container input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.float-container input.invalid {
border-color: #ff4444;
}
.float-container .error-message {
color: #ff4444;
font-size: 12px;
margin-top: 3px;
display: none;
}
.float-container button {
width: 100%;
padding: 8px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s;
}
.float-container button:hover {
background: #0056b3;
}
.status-message {
margin-top: 10px;
padding: 8px;
border-radius: 4px;
font-size: 13px;
display: none;
}
.status-success {
background-color: #dff0d8;
color: #3c763d;
display: block;
}
.status-error {
background-color: #f2dede;
color: #a94442;
display: block;
}
.debug-info {
margin-top: 10px;
font-size: 12px;
color: #666;
max-height: 100px;
overflow-y: auto;
display: none;
}
.show-debug {
display: block;
}
.content-wrapper {
transition: all 0.3s ease;
}
.float-container.collapsed .content-wrapper {
display: none;
}
.float-container.dragging {
transition: none !important;
opacity: 0.9;
}
`;
document.head.appendChild(style);
// 创建悬浮框
const floatContainer = document.createElement('div');
floatContainer.className = 'float-container';
floatContainer.innerHTML = `
<div class="drag-handle">拖动/双击收起</div>
<h3 class="title">数据上传配置</h3>
<div class="content-wrapper">
<div class="input-group">
<label for="upload-domain">上传域名</label>
<input type="text" id="upload-domain" placeholder="例如: http://127.0.0.1" value="http://127.0.0.1">
</div>
<div class="input-group">
<label for="storage-location">存放位置</label>
<input type="text" id="storage-location" placeholder="例如: A1-1-1" value="A1-1-1">
</div>
<div class="input-group">
<label for="stock-quantity">库存数量</label>
<input type="number" id="stock-quantity" placeholder="大于等于0的数字" min="0" value="0">
<div class="error-message" id="stock-error">请输入大于等于0的数字</div>
</div>
<button id="submit-btn">提取并上传数据</button>
<div class="status-message" id="status"></div>
<div class="debug-info" id="debug-info">调试信息将显示在这里</div>
</div>
`;
document.body.appendChild(floatContainer);
// 获取DOM元素
const dragHandle = floatContainer.querySelector('.drag-handle');
const domainInput = document.getElementById('upload-domain');
const locationInput = document.getElementById('storage-location');
const stockInput = document.getElementById('stock-quantity');
const stockError = document.getElementById('stock-error');
const submitBtn = document.getElementById('submit-btn');
const statusElement = document.getElementById('status');
const debugInfo = document.getElementById('debug-info');
// 双击和拖动事件处理 - 修复双击失灵问题
let isCollapsed = false;
let clickCount = 0;
let clickTimer = null;
let isDragging = false;
let lastClickTime = 0;
const DOUBLE_CLICK_DELAY = 300; // 延长双击检测时间到300ms
// 双击处理
dragHandle.addEventListener('click', function(e) {
// 如果正在拖动,不处理点击事件
if (isDragging) return;
const now = Date.now();
// 检查是否在双击时间窗口内
if (now - lastClickTime < DOUBLE_CLICK_DELAY) {
// 双击操作
e.preventDefault();
isCollapsed = !isCollapsed;
if (isCollapsed) {
floatContainer.classList.add('collapsed');
} else {
floatContainer.classList.remove('collapsed');
}
// 重置点击跟踪
clickCount = 0;
lastClickTime = 0;
return;
}
// 第一次点击,记录时间
lastClickTime = now;
clickCount = 1;
});
// 初始化拖动功能
makeDraggable(floatContainer);
// 库存输入验证
stockInput.addEventListener('input', function() {
const value = parseFloat(this.value);
if (isNaN(value) || value < 0) {
this.classList.add('invalid');
stockError.style.display = 'block';
return false;
} else {
this.classList.remove('invalid');
stockError.style.display = 'none';
return true;
}
});
// 提交按钮点击事件
submitBtn.addEventListener('click', function() {
// 显示调试信息
debugInfo.classList.add('show-debug');
debugInfo.innerHTML = '开始提取数据...<br>';
// 验证库存输入
const stockValue = parseFloat(stockInput.value);
if (isNaN(stockValue) || stockValue < 0) {
stockInput.classList.add('invalid');
stockError.style.display = 'block';
showDebug('库存数量输入无效');
return;
}
// 获取存放位置
const locationValue = locationInput.value.trim();
showDebug(`存放位置: ${locationValue}`);
// 获取域名并构建URL
let domain = domainInput.value.trim();
if (!domain) {
showStatus('请输入上传域名', 'error');
showDebug('未输入上传域名');
return;
}
if (domain.endsWith('/')) {
domain = domain.slice(0, -1);
}
const uploadUrl = `${domain}/api/post/components/`;
showDebug(`上传URL: ${uploadUrl}`);
// 提取各类数据
const tableData = extractTableData();
if (!tableData) {
showStatus('未找到有效表格数据', 'error');
return;
}
const flexData = extractFlexData();
if (!flexData) {
showDebug('未找到任何flex容器数据');
}
const imageUrls = extractImageUrls();
if (imageUrls.length > 0) {
showDebug(`成功提取 ${imageUrls.length} 个图片URL`);
} else {
showDebug('未找到任何图片URL');
}
const nameValue = extractNameData();
if (nameValue) {
showDebug(`提取到名称数据: ${nameValue}`);
} else {
showDebug('未找到名称数据');
}
const manualUrl = extractDataManualUrl();
if (manualUrl) {
showDebug(`提取到数据手册URL: ${manualUrl}`);
} else {
showDebug('未找到数据手册下载链接');
}
// 合并所有数据
const allData = {
...tableData,
...flexData,
stock: stockValue,
img: imageUrls,
name: nameValue,
'数据手册': manualUrl,
location: locationValue
};
showDebug(`合并后的数据总数: ${Object.keys(allData).length}`);
console.log('合并后的数据:', allData);
// 上传数据
uploadData(uploadUrl, allData);
});
// 提取表格数据
function extractTableData() {
const targetTables = document.querySelectorAll('table.w-full.caption-bottom.text-sm');
if (!targetTables || targetTables.length < 2) {
const message = `找到 ${targetTables ? targetTables.length : 0} 个目标表格,需要至少2个`;
showDebug(message);
return null;
}
showDebug(`找到 ${targetTables.length} 个目标表格,开始提取数据`);
const allTableData = {};
targetTables.forEach((table, tableIndex) => {
const tableBody = table.querySelector('tbody');
if (!tableBody) {
showDebug(`表格 ${tableIndex + 1} 中未找到tbody元素`);
return;
}
const rows = tableBody.querySelectorAll('tr');
if (rows.length === 0) {
showDebug(`表格 ${tableIndex + 1} 的tbody中未找到任何行数据`);
return;
}
showDebug(`表格 ${tableIndex + 1} 包含 ${rows.length} 行数据`);
rows.forEach((row, rowIndex) => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3) {
const key = cells[1].textContent.trim();
const value = cells[2].textContent.trim();
if (key && value) {
allTableData[key] = value;
showDebug(`表格 ${tableIndex + 1} 行 ${rowIndex + 1}: ${key} = ${value}`);
}
} else {
showDebug(`表格 ${tableIndex + 1} 行 ${rowIndex + 1}: 单元格数量不足3个,跳过`);
}
});
});
return Object.keys(allTableData).length > 0 ? allTableData : null;
}
// 提取flex容器中的dt/dd数据
function extractFlexData() {
const flexContainers = document.querySelectorAll('div.flex[class*="mt-\\[16px\\]"]');
if (!flexContainers || flexContainers.length === 0) {
showDebug('未找到任何flex容器');
return null;
}
showDebug(`找到 ${flexContainers.length} 个flex容器,开始提取数据`);
const flexData = {};
flexContainers.forEach((container, containerIndex) => {
const dtElements = container.querySelectorAll('dt');
if (dtElements.length === 0) {
showDebug(`flex容器 ${containerIndex + 1} 中未找到dt元素`);
return;
}
dtElements.forEach((dt, dtIndex) => {
const key = dt.textContent.trim();
if (!key) {
showDebug(`flex容器 ${containerIndex + 1} 中dt ${dtIndex + 1} 为空`);
return;
}
let ddElement = dt.nextElementSibling;
if (ddElement && ddElement.tagName.toLowerCase() !== 'dd') {
ddElement = dt.parentElement.querySelector('dd');
}
if (ddElement && ddElement.tagName.toLowerCase() === 'dd') {
const ddText = ddElement.textContent.trim();
let value = ddText;
if (key.includes('参考')) {
showDebug(`flex容器 ${containerIndex + 1} dt ${dtIndex + 1}: 发现包含"参考"的dt内容`);
const aTag = ddElement.querySelector('a');
if (aTag && aTag.href) {
value = aTag.href;
showDebug(`flex容器 ${containerIndex + 1} dt ${dtIndex + 1}: 提取a标签href: ${value}`);
} else {
showDebug(`flex容器 ${containerIndex + 1} dt ${dtIndex + 1}: 未找到a标签,使用dd文本值`);
}
}
flexData[key] = value;
showDebug(`flex容器 ${containerIndex + 1} dt ${dtIndex + 1}: ${key} = ${value}`);
} else {
showDebug(`flex容器 ${containerIndex + 1} 中dt ${dtIndex + 1} (${key}) 未找到对应的dd元素`);
}
});
});
return Object.keys(flexData).length > 0 ? flexData : null;
}
// 提取图片URL
function extractImageUrls() {
const imageUrls = [];
const targetUl = document.querySelector('ul.flex-1.flex.items-center.justify-center');
if (!targetUl) {
showDebug('未找到class为"flex-1 flex items-center justify-center"的ul元素');
return imageUrls;
}
const listItems = targetUl.querySelectorAll('li');
if (!listItems || listItems.length === 0) {
showDebug('找到目标ul,但未包含任何li元素');
return imageUrls;
}
showDebug(`找到目标ul,包含 ${listItems.length} 个li元素,开始提取图片URL`);
listItems.forEach((li, index) => {
const imgElement = li.querySelector('img');
if (imgElement && imgElement.src) {
let imgUrl = imgElement.src.replace('breviary', 'source');
imageUrls.push(imgUrl);
showDebug(`li元素 ${index + 1}: 提取并处理URL - ${imgUrl}`);
} else {
showDebug(`li元素 ${index + 1}: 未找到有效img元素或src属性`);
}
});
return imageUrls;
}
// 提取名称数据
function extractNameData() {
const nameParagraph = document.querySelector('div.text-\\[14px\\].mb-\\[20px\\] p');
if (!nameParagraph) {
showDebug('未找到class为"text-[14px] mb-[20px]"的div下的p标签,尝试通用选择器');
const generalParagraph = document.querySelector('.text-\\[14px\\].mb-\\[20px\\] p');
if (generalParagraph) {
return generalParagraph.textContent.trim();
}
return null;
}
return nameParagraph.textContent.trim();
}
// 提取数据手册PDF下载链接
function extractDataManualUrl() {
const pdfLink = document.querySelector('a#item-pdf-down');
if (!pdfLink) {
showDebug('未找到id为"item-pdf-down"的a标签');
return null;
}
if (!pdfLink.href) {
showDebug('找到id为"item-pdf-down"的a标签,但未包含href属性');
return null;
}
return pdfLink.href;
}
// 上传数据
function uploadData(url, data) {
showDebug('开始上传数据...');
showStatus('正在上传数据...', 'success');
GM_xmlhttpRequest({
method: 'POST',
url: url,
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify(data),
onload: function(response) {
console.log('数据上传成功,服务器响应:', response.responseText);
showDebug(`上传成功,服务器响应状态: ${response.status}`);
showStatus('数据上传成功!', 'success');
},
onerror: function(error) {
console.error('数据上传失败:', error);
showDebug(`上传失败: ${error.message}`);
showStatus('数据上传失败', 'error');
},
onabort: function() {
console.log('请求已中止');
showDebug('请求已中止');
showStatus('请求已中止', 'error');
},
ontimeout: function() {
console.log('请求超时');
showDebug('请求超时');
showStatus('请求超时', 'error');
}
});
}
// 显示状态消息
function showStatus(message, type) {
statusElement.textContent = message;
statusElement.className = 'status-message';
statusElement.classList.add(`status-${type}`);
if (type === 'success') {
setTimeout(() => {
statusElement.className = 'status-message';
}, 3000);
}
}
// 显示调试信息
function showDebug(message) {
const timestamp = new Date().toLocaleTimeString();
debugInfo.innerHTML += `[${timestamp}] ${message}<br>`;
debugInfo.scrollTop = debugInfo.scrollHeight;
}
// 拖动功能实现 - 兼顾双击识别
function makeDraggable(element) {
let offsetX, offsetY;
const handle = element.querySelector('.drag-handle');
let dragStartX, dragStartY;
const DRAG_THRESHOLD = 5; // 拖动阈值,像素
function startDrag(e) {
// 记录初始拖动位置
dragStartX = e.clientX;
dragStartY = e.clientY;
const rect = element.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
// 使用捕获阶段监听
document.addEventListener('mousemove', drag, true);
document.addEventListener('mouseup', stopDrag, true);
handle.style.cursor = 'grabbing';
}
function drag(e) {
// 检查是否超过拖动阈值,避免误判为拖动
const dx = Math.abs(e.clientX - dragStartX);
const dy = Math.abs(e.clientY - dragStartY);
if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) {
isDragging = true;
element.classList.add('dragging');
e.preventDefault();
e.stopPropagation();
// 计算新位置
const newX = e.clientX - offsetX;
const newY = e.clientY - offsetY;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const elementWidth = element.offsetWidth;
const elementHeight = element.offsetHeight;
// 边界限制
const boundedX = Math.max(0, Math.min(newX, viewportWidth - elementWidth));
const boundedY = Math.max(0, Math.min(newY, viewportHeight - elementHeight));
// 设置新位置
element.style.left = boundedX + 'px';
element.style.top = boundedY + 'px';
}
}
function stopDrag() {
// 重置拖动状态
isDragging = false;
element.classList.remove('dragging');
document.removeEventListener('mousemove', drag, true);
document.removeEventListener('mouseup', stopDrag, true);
handle.style.cursor = 'move';
}
// 鼠标按下时启动拖动检测
handle.addEventListener('mousedown', startDrag);
}
})();