Greasy Fork

Greasy Fork is available in English.

店小秘助手

当前为 2025-10-01 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
})();