店小秘助手

// ==UserScript==
// @name         店小秘助手
// @namespace    http://tampermonkey.net/
// @version      1.6
// @description  无
// @license MIT
// @author       Rayu
// @match        https://www.dianxiaomi.com/web/shopeeSite/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    function waitForContainer(selector, timeout = 5000) {
        return new Promise((resolve, reject) => {
            const intervalTime = 100;
            let totalTime = 0;

            const interval = setInterval(() => {
                const el = document.querySelector(selector);
                if (el) {
                    clearInterval(interval);
                    resolve(el);
                }
                totalTime += intervalTime;
                if (totalTime >= timeout) {
                    clearInterval(interval);
                    reject(new Error('未找到目标容器:' + selector));
                }
            }, intervalTime);
        });
    }

    waitForContainer('.d-grid-pager--header-left.min-w-0')
        .then(container => {
            if (container.querySelector('#custom-filter-box')) return;

            const filterDiv = document.createElement('div');
            filterDiv.id = 'custom-filter-box';
            filterDiv.style.padding = '6px 12px';
            filterDiv.style.backgroundColor = '#f9f9f9';
            filterDiv.style.border = '1px solid #ccc';
            filterDiv.style.borderRadius = '4px';
            filterDiv.style.display = 'flex';
            filterDiv.style.alignItems = 'center';
            filterDiv.style.gap = '8px';
            filterDiv.style.minWidth = '450px';

            const input = document.createElement('input');
            input.type = 'text';
            input.placeholder = '输入筛选关键词(如: another in your shop)';
            input.style.flex = '1';
            input.style.height = '28px';
            input.style.padding = '0 8px';
            input.style.border = '1px solid #ccc';
            input.style.borderRadius = '4px';

            const button = document.createElement('button');
            button.textContent = '筛选';
            button.style.height = '28px';
            button.style.padding = '0 12px';
            button.style.cursor = 'pointer';
            button.style.border = '1px solid #1890ff';
            button.style.backgroundColor = '#1890ff';
            button.style.color = '#fff';
            button.style.borderRadius = '4px';
            button.style.fontSize = '14px';

            const selectAllBtn = document.createElement('button');
            selectAllBtn.textContent = '全选';
            selectAllBtn.style.height = '28px';
            selectAllBtn.style.padding = '0 12px';
            selectAllBtn.style.cursor = 'pointer';
            selectAllBtn.style.border = '1px solid #52c41a';
            selectAllBtn.style.backgroundColor = '#52c41a';
            selectAllBtn.style.color = '#fff';
            selectAllBtn.style.borderRadius = '4px';
            selectAllBtn.style.fontSize = '14px';

            const unselectAllBtn = document.createElement('button');
            unselectAllBtn.textContent = '取消全选';
            unselectAllBtn.style.height = '28px';
            unselectAllBtn.style.padding = '0 12px';
            unselectAllBtn.style.cursor = 'pointer';
            unselectAllBtn.style.border = '1px solid #f5222d';
            unselectAllBtn.style.backgroundColor = '#f5222d';
            unselectAllBtn.style.color = '#fff';
            unselectAllBtn.style.borderRadius = '4px';
            unselectAllBtn.style.fontSize = '14px';

            const invertSelectBtn = document.createElement('button');
            invertSelectBtn.textContent = '反选';
            invertSelectBtn.style.height = '28px';
            invertSelectBtn.style.padding = '0 12px';
            invertSelectBtn.style.cursor = 'pointer';
            invertSelectBtn.style.border = '1px solid #faad14';
            invertSelectBtn.style.backgroundColor = '#faad14';
            invertSelectBtn.style.color = '#fff';
            invertSelectBtn.style.borderRadius = '4px';
            invertSelectBtn.style.fontSize = '14px';

            // 新增“打开编辑页”按钮
            const openSelectedBtn = document.createElement('button');
            openSelectedBtn.textContent = '打开编辑页';
            openSelectedBtn.style.height = '28px';
            openSelectedBtn.style.padding = '0 12px';
            openSelectedBtn.style.cursor = 'pointer';
            openSelectedBtn.style.border = '1px solid #13c2c2';
            openSelectedBtn.style.backgroundColor = '#13c2c2';
            openSelectedBtn.style.color = '#fff';
            openSelectedBtn.style.borderRadius = '4px';
            openSelectedBtn.style.fontSize = '14px';

            filterDiv.appendChild(input);
            filterDiv.appendChild(button);
            filterDiv.appendChild(selectAllBtn);
            filterDiv.appendChild(unselectAllBtn);
            filterDiv.appendChild(invertSelectBtn);
            filterDiv.appendChild(openSelectedBtn);

            container.appendChild(filterDiv);

            // 筛选按钮逻辑:关键词匹配失败原因中span文本
            button.addEventListener('click', () => {
                const keyword = input.value.trim();
                if (!keyword) {
                    document.querySelectorAll('tr.vxe-body--row').forEach(row => {
                        row.style.display = '';
                    });
                    return;
                }
                document.querySelectorAll('tr.vxe-body--row').forEach(row => {
                    const errorMsgElem = row.querySelector('.product-list-error-msg span');
                    if (errorMsgElem && errorMsgElem.textContent.includes(keyword)) {
                        row.style.display = '';
                    } else {
                        row.style.display = 'none';
                    }
                });
            });

            // 全选:显示的行选中(如果没选中才点checkbox)
            selectAllBtn.addEventListener('click', () => {
                document.querySelectorAll('tr.vxe-body--row').forEach(row => {
                    if (row.style.display !== 'none') {
                        const checkbox = row.querySelector('input.ant-checkbox-input[type="checkbox"]');
                        if (checkbox && !checkbox.checked) {
                            checkbox.click();
                        }
                    }
                });
            });

            // 取消全选:全部复选框取消选中
            unselectAllBtn.addEventListener('click', () => {
                document.querySelectorAll('tr.vxe-body--row').forEach(row => {
                    const checkbox = row.querySelector('input.ant-checkbox-input[type="checkbox"]');
                    if (checkbox && checkbox.checked) {
                        checkbox.click();
                    }
                });
            });

            // 反选:显示的行复选框状态取反
            invertSelectBtn.addEventListener('click', () => {
                document.querySelectorAll('tr.vxe-body--row').forEach(row => {
                    const checkbox = row.querySelector('input.ant-checkbox-input[type="checkbox"]');
                    if (checkbox && row.style.display !== 'none') {
                        checkbox.click();
                    }
                });
            });

            // 打开编辑页按钮:批量打开所有选中行的“编辑”链接,带延迟避免浏览器拦截
            openSelectedBtn.addEventListener('click', () => {
                const rows = Array.from(document.querySelectorAll('tr.vxe-body--row'));
                const selectedRows = rows.filter(row => {
                    const checkbox = row.querySelector('input.ant-checkbox-input[type="checkbox"]');
                    return checkbox && checkbox.checked;
                });

                if (selectedRows.length === 0) {
                    alert('未选中任何商品或未找到可打开的编辑链接');
                    return;
                }

                let openedCount = 0;
                selectedRows.forEach((row, index) => {
                    setTimeout(() => {
                        const editLinkElem = row.querySelector('td[colid="col_13"] a[href*="/edit?id="]');
                        if (editLinkElem && editLinkElem.href) {
                            const url = new URL(editLinkElem.getAttribute('href'), location.origin);
                            window.open(url.href, '_blank');
                            openedCount++;
                        } else {
                            console.warn('未找到编辑链接,无法打开', row);
                        }

                        if (index === selectedRows.length -1) {
                            setTimeout(() => {
                                alert(`已尝试打开${openedCount}个编辑页,请检查弹窗拦截。`);
                            }, 300);
                        }
                    }, 300 * index);
                });
            });

        })
        .catch(err => {
            console.error(err);
        });
    // ====================== 核心配置:两种结构的表头关键词(无需修改) ======================
    const HEADER_CONFIG = {
        // 结构A:顏色+尺寸(无款式)
        colorSize: {
            colorKey: '顏色',  // 顏色表头(繁体)
            sizeKey: '尺寸'    // 尺寸表头
        },
        // 结构B:仅款式(无顏色/尺寸)
        styleOnly: {
            styleKey: '款式'   // 款式表头
        }
    };
    console.log("📌 双结构配置:支持「顏色+尺寸」和「仅款式」两种表格", HEADER_CONFIG);

    // ====================== 1. 等待表格加载+识别表头结构 ======================
    function waitAndDetectTableStructure(timeout = 20000) {
        return new Promise((resolve) => {
            let waitTime = 0;
            const checkInterval = 1000;

            const checkTimer = setInterval(() => {
                waitTime += checkInterval;
                console.log(`等待表格:已等待${waitTime/1000}秒,正在检测表头结构`);

                // 步骤1:找核心容器#skuDataInfo
                const skuContainer = document.querySelector('#skuDataInfo');
                if (!skuContainer) {
                    if (waitTime >= timeout) { clearInterval(checkTimer); resolve(null); }
                    return;
                }

                // 步骤2:找表格(需有表头和产品行)
                const productTable = skuContainer.querySelector('table');
                if (!productTable || !productTable.querySelector('thead') || !productTable.querySelector('tbody')) {
                    if (waitTime >= timeout) { clearInterval(checkTimer); resolve(null); }
                    return;
                }

                // 步骤3:确认有产品行(至少1行)
                const productRows = productTable.querySelectorAll('tbody tr');
                if (productRows.length === 0) {
                    if (waitTime >= timeout) { clearInterval(checkTimer); resolve(null); }
                    return;
                }

                // 步骤4:关键!检测表格属于哪种结构
                const tableStructure = detectHeaderStructure(productTable);
                if (tableStructure.type !== 'unknown') { // 识别到有效结构
                    clearInterval(checkTimer);
                    console.log(`✅ 表格就绪:共${productRows.length}行,检测到表头结构→${tableStructure.type}`);
                    resolve({
                        skuContainer,
                        productTable,
                        productRows,
                        tableStructure // 携带结构信息(类型+列索引)
                    });
                }

                // 超时容错:即使未识别结构,也返回当前状态
                if (waitTime >= timeout) {
                    clearInterval(checkTimer);
                    const tableStructure = detectHeaderStructure(productTable);
                    resolve({
                        skuContainer,
                        productTable,
                        productRows,
                        tableStructure
                    });
                }
            }, checkInterval);
        });
    }

    // ====================== 2. 核心函数:检测表格属于哪种结构 ======================
    function detectHeaderStructure(table) {
        const headers = Array.from(table.querySelectorAll('thead th, thead td'));
        const structure = { type: 'unknown', indexes: {} }; // unknown=未识别

        // 先检测是否为「结构A:顏色+尺寸」(优先级:先找颜色+尺寸,再找款式)
        const colorIdx = findHeaderIndex(headers, HEADER_CONFIG.colorSize.colorKey);
        const sizeIdx = findHeaderIndex(headers, HEADER_CONFIG.colorSize.sizeKey);
        if (colorIdx !== -1 && sizeIdx !== -1) {
            structure.type = 'colorSize'; // 结构A标识
            structure.indexes = { colorIdx, sizeIdx }; // 颜色/尺寸列索引
            console.log(`🔍 检测到结构A(顏色+尺寸):顏色列第${colorIdx+1}列,尺寸列第${sizeIdx+1}列`);
            return structure;
        }

        // 再检测是否为「结构B:仅款式」
        const styleIdx = findHeaderIndex(headers, HEADER_CONFIG.styleOnly.styleKey);
        if (styleIdx !== -1) {
            structure.type = 'styleOnly'; // 结构B标识
            structure.indexes = { styleIdx }; // 款式列索引
            console.log(`🔍 检测到结构B(仅款式):款式列第${styleIdx+1}列`);
            return structure;
        }

        // 两种结构都未识别
        console.error("❌ 未识别表头结构:既无「顏色+尺寸」,也无「款式」表头");
        return structure;
    }

    // ====================== 3. 辅助函数:根据关键词找表头索引(兼容空格) ======================
    function findHeaderIndex(headers, targetKey) {
        for (let i = 0; i < headers.length; i++) {
            const headerText = headers[i].textContent.trim().replace(/\s+/g, ''); // 去所有空格
            if (headerText === targetKey) {
                return i; // 返回列索引(0开始)
            }
        }
        return -1; // 未找到返回-1
    }

    // ====================== 4. 按表格结构提取数据(分两种情况) ======================
    function extractDataByStructure(productRows, tableStructure) {
        const priceList = [];
        const { type, indexes } = tableStructure;

        // 情况1:结构A(顏色+尺寸)
        if (type === 'colorSize') {
            const { colorIdx, sizeIdx } = indexes;
            productRows.forEach((row, idx) => {
                try {
                    const tds = row.querySelectorAll('td');
                    if (tds.length === 0) return;

                    // 提取顏色
                    const color = (colorIdx < tds.length)
                        ? tds[colorIdx].textContent.trim().replace(/\s+/g, ' ')
                        : '';
                    // 提取尺寸
                    const size = (sizeIdx < tds.length)
                        ? tds[sizeIdx].textContent.trim().replace(/\s+/g, ' ')
                        : '';
                    // 提取价格
                    const priceEl = row.querySelector('.font-size-10\\!.text-left.ml-10');
                    if (!priceEl) return;
                    const priceText = priceEl.textContent.trim();
                    const priceMatch = priceText.match(/≈\s*(\d+\.\d+)\s*USD/);
                    if (!priceMatch) return;

                    // 规格:顏色-尺寸(无尺寸时只显顏色)
                    let spec = color;
                    if (size) spec = `${color} - ${size}`;
                    spec = spec || '未识别规格';

                    priceList.push({ value: parseFloat(priceMatch[1]), text: priceText, spec });
                    console.log(`✅ 第${idx+1}行(结构A):价格=${priceText} | 规格=${spec}`);
                } catch (e) {
                    console.warn(`第${idx+1}行(结构A):提取出错→${e.message}`);
                }
            });
        }

        // 情况2:结构B(仅款式)
        else if (type === 'styleOnly') {
            const { styleIdx } = indexes;
            productRows.forEach((row, idx) => {
                try {
                    const tds = row.querySelectorAll('td');
                    if (tds.length === 0) return;

                    // 提取款式
                    const style = (styleIdx < tds.length)
                        ? tds[styleIdx].textContent.trim().replace(/\s+/g, ' ')
                        : '';
                    // 提取价格
                    const priceEl = row.querySelector('.font-size-10\\!.text-left.ml-10');
                    if (!priceEl) return;
                    const priceText = priceEl.textContent.trim();
                    const priceMatch = priceText.match(/≈\s*(\d+\.\d+)\s*USD/);
                    if (!priceMatch) return;

                    // 规格:款式
                    const spec = style || '未识别款式';

                    priceList.push({ value: parseFloat(priceMatch[1]), text: priceText, spec });
                    console.log(`✅ 第${idx+1}行(结构B):价格=${priceText} | 规格=款式:${spec}`);
                } catch (e) {
                    console.warn(`第${idx+1}行(结构B):提取出错→${e.message}`);
                }
            });
        }

        return priceList.length > 0 ? priceList : null;
    }

    // ====================== 5. 计算最值+插入容器(按结构显示规格) ======================
    function getMinMax(priceList) {
        const sorted = [...priceList].sort((a, b) => a.value - b.value);
        const min = sorted[0];
        const maxVal = sorted.at(-1).value;
        const maxSpecs = priceList.filter(item => item.value.toFixed(2) === maxVal.toFixed(2)).map(item => item.spec);
        return { min: { text: min.text, spec: min.spec }, max: { text: sorted.at(-1).text, specs: maxSpecs } };
    }

    function insertContainer(priceMinMax, skuContainer, tableStructure) {
        const oldContainer = document.getElementById('price-range-container');
        if (oldContainer) oldContainer.remove();

        const container = document.createElement('div');
        container.id = "price-range-container";
        container.style.cssText = `
            margin: 15px; padding: 12px 15px; background: #f8f9fa;
            border: 2px solid #e9ecef; border-radius: 6px; font-size: 12px;
            color: #333; z-index: 9999; position: relative;
        `;

        // 按结构显示规格标题(结构A显“顏色-尺寸”,结构B显“款式”)
        const specTitle = tableStructure.type === 'colorSize' ? '规格(顏色-尺寸)' : '规格(款式)';
        container.innerHTML = `
            <div style="font-size:14px; font-weight:bold; margin-bottom:8px; color:#1890ff;">#skuDataInfo 价格范围(USD)</div>
            <div style="margin-bottom:10px; line-height:1.6;">
                <div style="color:#28a745;">🔻 最小值:${priceMinMax.min.text}</div>
                <div style="padding-left:18px; margin-top:3px; color:#666; font-size:11px;">${specTitle}:${priceMinMax.min.spec}</div>
            </div>
            <div style="line-height:1.6;">
                <div style="color:#dc3545;">🔺 最大值:${priceMinMax.max.text}</div>
                <div style="padding-left:18px; margin-top:3px; color:#666; font-size:11px;">
                    ${priceMinMax.max.specs.map(s => `- ${specTitle}:${s}`).join('<br>')}
                </div>
            </div>
        `;

        const formContent = skuContainer.querySelector('.form-card-content') || skuContainer;
        formContent.prepend(container);
        console.log(`✅ 价格容器已插入(适配结构:${tableStructure.type})`);
    }

    // ====================== 6. 主流程:自动识别→提取→显示 ======================
    async function main() {
        try {
            // 步骤1:等待表格+识别结构
            const data = await waitAndDetectTableStructure();
            if (!data || !data.productTable || data.productRows.length === 0 || data.tableStructure.type === 'unknown') {
                console.error("❌ 无有效表格/产品行,或未识别表头结构,脚本终止");
                return;
            }
            const { skuContainer, productRows, tableStructure } = data;

            // 步骤2:按结构提取数据
            const priceList = extractDataByStructure(productRows, tableStructure);
            if (!priceList) {
                console.error("❌ 未提取到有效价格数据");
                return;
            }

            // 步骤3:显示价格范围
            const priceMinMax = getMinMax(priceList);
            insertContainer(priceMinMax, skuContainer, tableStructure);

        } catch (e) {
            console.error("❌ 脚本主流程出错:", e);
        }
    }

    // 启动
    main();
})();

QingJ © 2025

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