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