元器件信息提取

提取表格、flex容器、图片URL、名称、PDF下载链接及存放位置并上传

// ==UserScript==
// @name         元器件信息提取
// @namespace    http://tampermonkey.net/
// @version      1.8.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;

    // 双击处理(新增日志清理功能)
    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');
                // 收起时清理调试日志
                debugInfo.innerHTML = '';
                debugInfo.classList.remove('show-debug');
                showDebug('面板已收起,日志已清理');
            } 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);
        }
        // 关键修改:将/api/post/components/改为/api/records/
        const uploadUrl = `${domain}/api/records/`;
        showDebug(`上传URL: ${uploadUrl}`);

        // 提取各类数据
        const tableData = extractTableData() || {};
        const flexData = extractFlexData() || {};
        const imageUrls = extractImageUrls();
        const nameValue = extractNameData() || '';
        const manualUrl = extractDataManualUrl() || '';
        const priceValue = extractPrice() || '';

        // 合并所有原始数据
        const allRawData = {
            ...tableData,
            ...flexData,
            '数据手册': manualUrl
        };
        showDebug(`原始数据总数: ${Object.keys(allRawData).length}`);

        // 按需求组装新数据结构
        const formattedData = assembleData(
            allRawData,
            nameValue,
            priceValue,
            imageUrls,
            locationValue,
            stockValue
        );

        showDebug('数据组装完成,结构如下:');
        showDebug(JSON.stringify(formattedData, null, 2));
        console.log('组装后的数据:', formattedData);

        // 上传数据
        uploadData(uploadUrl, formattedData);
    });

    // 数据组装核心函数
    function assembleData(rawData, name, price, images, location, stock) {
        // 1. 初始化基础结构
        const result = {
            name: name,
            price: price,
            model: '',           // 商品型号
            category: '',        // 商品目录
            fengzhuang: '',      // 商品封装(处理后)
            location: location,
            stock: stock,
            image_url: '',       // img列表第一个元素
            description: '',     // 描述
            specifications: []   // 剩余数据
        };

        // 2. 提取指定字段
        const processedKeys = new Set([
            'name', 'price', 'model', 'category',
            'fengzhuang', 'location', 'stock',
            'image_url', 'description'
        ]);

        // 提取商品型号
        if (rawData['商品型号']) {
            result.model = rawData['商品型号'];
            processedKeys.add('商品型号');
        }

        // 提取商品目录
        if (rawData['商品目录']) {
            result.category = rawData['商品目录'];
            processedKeys.add('商品目录');
        }

        // 提取商品封装(已通过flexData处理,直接使用)
        if (rawData['商品封装']) {
            result.fengzhuang = rawData['商品封装'];
            showDebug(`使用提取到的商品封装值: "${result.fengzhuang}"`);
            processedKeys.add('商品封装');
        }

        // 提取描述
        if (rawData['描述']) {
            result.description = rawData['描述'];
            processedKeys.add('描述');
        }

        // 处理图片
        if (images.length > 0) {
            // 第一个图片作为主图
            result.image_url = images[0];

            // 剩余图片按"图片1"、"图片2"格式添加到specifications
            images.slice(1).forEach((img, index) => {
                result.specifications.push({
                    key: `图片${index + 1}`,
                    value: img
                });
            });
        }

        // 3. 处理剩余数据,添加到specifications
        Object.entries(rawData).forEach(([key, value]) => {
            if (!processedKeys.has(key)) {
                result.specifications.push({
                    key: key,
                    value: value.toString()
                });
            }
        });

        return result;
    }

    // 提取表格数据
    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数据(商品封装使用title属性)
    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') {
                    let value;

                    // 规则:如果dt是"商品封装",优先使用dd的title属性
                    if (key === '商品封装') {
                        // 检查dd元素是否有title属性
                        if (ddElement.title && ddElement.title.trim()) {
                            value = ddElement.title.trim();
                            showDebug(`flex容器 ${containerIndex + 1} dt ${dtIndex + 1}: 商品封装使用dd的title值 → "${value}"`);
                        } else {
                            // 如果没有title属性,使用文本内容
                            value = ddElement.textContent.trim();
                            showDebug(`flex容器 ${containerIndex + 1} dt ${dtIndex + 1}: 商品封装没有title属性,使用文本值 → "${value}"`);
                        }
                    }
                    // 原有逻辑:处理包含"参考"的dt
                    else 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 {
                            value = ddElement.textContent.trim();
                            showDebug(`flex容器 ${containerIndex + 1} dt ${dtIndex + 1}: 未找到a标签,使用dd文本值`);
                        }
                    }
                    // 其他情况使用文本内容
                    else {
                        value = ddElement.textContent.trim();
                        showDebug(`flex容器 ${containerIndex + 1} dt ${dtIndex + 1}: 使用dd文本值 → "${value}"`);
                    }

                    flexData[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 extractPrice() {
        // 获取所有class为"flex-1 text-[#000000]"的span元素
        const elements = document.querySelectorAll('span.flex-1.text-\\[\\#000000\\]');

        if (!elements || elements.length < 2) {
            showDebug(`找到 ${elements ? elements.length : 0} 个class为"flex-1 text-[#000000]"的span元素,需要至少2个`);
            return null;
        }

        // 获取第二个元素的文本内容
        const secondElementText = elements[1].textContent.trim();
        showDebug(`第二个class为"flex-1 text-[#000000]"的span元素完整文本: "${secondElementText}"`);

        // 检查文本长度是否足够
        if (secondElementText.length < 2) {
            showDebug(`第二个span元素文本长度不足,无法提取从第二个字符开始的内容`);
            return null;
        }

        // 提取从第二个字符开始的所有内容(忽略第一个价格符号)
        const priceValue = secondElementText.substring(1).trim();
        showDebug(`提取到的价格数据(去除第一个字符后): ${priceValue}`);

        return priceValue;
    }

    // 上传数据
    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}`);

                // 检查HTTP状态码,2xx范围为成功
                if (response.status >= 200 && response.status < 300) {
                    showDebug('上传成功,服务器返回成功状态码');
                    showStatus('数据上传成功!', 'success');
                } else if (response.status === 404) {
                    showDebug('上传失败,服务器返回404 Not Found');
                    showStatus('上传失败:服务器未找到请求的资源(404)', 'error');
                } else {
                    showDebug(`上传失败,服务器返回非成功状态码: ${response.status}`);
                    showStatus(`上传失败:服务器返回错误状态 ${response.status}`, 'error');
                }
            },
            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);
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址