// ==UserScript==
// @name 工作室买买买!
// @namespace http://tampermonkey.net/
// @version 2.3
// @description 跨页面收集商品信息,支持批量导出和清空
// @author MADAO_Mu
// @match https://item.taobao.com/*
// @match https://detail.tmall.com/*
// @match https://item.jd.com/*
// @grant GM_setClipboard
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
(function() {
'use strict';
// 页面内通知
function showPageNotification(title, message, type = 'success') {
const notification = document.createElement('div');
notification.style.position = 'fixed';
notification.style.top = '20px';
notification.style.right = '20px';
notification.style.zIndex = '9999';
notification.style.padding = '15px 20px';
notification.style.borderRadius = '5px';
notification.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)';
notification.style.fontSize = '14px';
notification.style.transition = 'all 0.3s ease';
notification.style.opacity = '0';
notification.style.transform = 'translateY(-20px)';
notification.style.cursor = 'default';
if (type === 'success') {
notification.style.backgroundColor = '#4CAF50';
notification.style.color = 'white';
} else if (type === 'error') {
notification.style.backgroundColor = '#F44336';
notification.style.color = 'white';
} else if (type === 'info') {
notification.style.backgroundColor = '#2196F3';
notification.style.color = 'white';
}
const titleElement = document.createElement('div');
titleElement.style.fontWeight = 'bold';
titleElement.style.marginBottom = '5px';
titleElement.textContent = title;
const messageElement = document.createElement('div');
messageElement.textContent = message;
notification.appendChild(titleElement);
notification.appendChild(messageElement);
document.body.appendChild(notification);
setTimeout(() => {
notification.style.opacity = '1';
notification.style.transform = 'translateY(0)';
}, 10);
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transform = 'translateY(-20px)';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}, 3000);
notification.addEventListener('click', () => {
notification.style.opacity = '0';
notification.style.transform = 'translateY(-20px)';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
});
}
// 精简URL
function getCleanUrl(url) {
try {
const parsedUrl = new URL(url);
const hostname = parsedUrl.hostname;
// 京东商品页面
const isJDItemPage = hostname.includes('jd.com') &&
/\/(\d+)\.html$/.test(parsedUrl.pathname);
if (isJDItemPage) {
const match = parsedUrl.pathname.match(/\/(\d+)\.html$/);
const productId = match ? match[1] : null;
if (productId) {
return `https://item.jd.com/${productId}.html`;
}
}
// 淘宝天猫商品页面
const isTaobaoItemPage = (
(hostname.includes('taobao.com') || hostname.includes('tmall.com')) &&
(parsedUrl.pathname.includes('item.htm') || parsedUrl.pathname.includes('item/'))
);
if (isTaobaoItemPage) {
const productId = parsedUrl.searchParams.get('id');
const skuId = parsedUrl.searchParams.get('skuId');
if (productId) {
let cleanUrl = hostname.includes('tmall.com')
? `https://detail.tmall.com/item.htm?id=${productId}`
: `https://item.taobao.com/item.htm?id=${productId}`;
if (skuId) {
cleanUrl += `&skuId=${skuId}`;
}
return cleanUrl;
}
}
} catch (error) {
console.error("URL解析错误:", error);
}
return url;
}
// 提取商品信息
function extractProductInfo() {
const productInfo = {
title: '',
price: '',
specs: {},
url: getCleanUrl(window.location.href)
};
// 提取商品标题
const jdTitleElement = document.querySelector('.sku-name-title') || document.querySelector('.sku-name');
const tbTitleElement = document.querySelector('.mainTitle--ocKo1xwj') ||
document.querySelector('[class*="mainTitle"]');
if (jdTitleElement) {
// 移除所有图片元素,只获取文本
const titleText = Array.from(jdTitleElement.childNodes)
.filter(node => node.nodeType === Node.TEXT_NODE)
.map(node => node.textContent.trim())
.join(' ')
.replace(/\s+/g, ' ');
productInfo.title = titleText;
} else if (tbTitleElement) {
const titleText = Array.from(tbTitleElement.childNodes)
.filter(node => node.nodeType === Node.TEXT_NODE)
.map(node => node.textContent.trim())
.join(' ')
.replace(/\s+/g, ' ');
productInfo.title = titleText || tbTitleElement.getAttribute('title') || '';
}
// 提取价格(使用原价)
const jdPriceSelectors = [
'#J_DailyPrice .price', // 日常价格
'.p-price .price' // 主价格
];
for (const selector of jdPriceSelectors) {
const priceElement = document.querySelector(selector);
if (priceElement) {
const priceText = priceElement.textContent.trim();
const cleanPrice = priceText.replace(/[^\d.]/g, '');
productInfo.price = cleanPrice;
break;
}
}
// 淘宝/天猫价格
if (!productInfo.price) {
// 先尝试获取优惠前价格(原价)
const displayPriceContainer = document.querySelector('[class*="displayPrice--"]');
if (displayPriceContainer) {
const priceWrapContainer = displayPriceContainer.querySelector('[class*="priceWrap--"]');
if (priceWrapContainer) {
// 在subPrice中查找价格(通常是原价)
const subPriceContainer = priceWrapContainer.querySelector('[class*="subPrice--"]');
if (subPriceContainer) {
// 获取所有text元素
const textElements = subPriceContainer.querySelectorAll('[class*="text--"]');
// 遍历所有text元素,找到不包含¥的元素
for (const element of textElements) {
const text = element.textContent.trim();
if (!text.includes('¥')) {
productInfo.price = text.replace(/[^\d.]/g, '');
break;
}
}
}
}
}
// 如果没找到原价,再尝试获取normalPrice
if (!productInfo.price) {
const normalPriceContainer = document.querySelector('[class*="normalPrice--"]');
if (normalPriceContainer) {
const priceText = normalPriceContainer.querySelector('[class*="text--"]');
if (priceText) {
productInfo.price = priceText.textContent.trim().replace(/[^\d.]/g, '');
}
}
}
// 如果还是没找到价格,尝试其他价格
if (!productInfo.price) {
const priceElements = document.querySelectorAll('[class*="text"]');
if (priceElements.length > 0) {
productInfo.price = priceElements[priceElements.length === 1 ? 0 : 1].textContent.trim().replace(/[^\d.]/g, '');
}
}
}
// 提取规格
// 京东规格
const specContainer = document.querySelector('#choose-attrs');
if (specContainer) {
const specTypes = specContainer.querySelectorAll('.li.p-choose');
specTypes.forEach(specType => {
const labelElement = specType.querySelector('.dt');
const selectedItem = specType.querySelector('.item.selected');
if (labelElement && selectedItem) {
const label = labelElement.textContent.trim().replace(/选择|:|\s/g, '');
const value = selectedItem.getAttribute('data-value') ||
selectedItem.textContent.trim();
productInfo.specs[label] = value;
}
});
}
// 淘宝/天猫规格 - 支持多种前缀
if (Object.keys(productInfo.specs).length === 0) {
// 通用选择器,匹配多种可能的前缀
const skuItems = document.querySelectorAll('[class*="skuItem--"]');
skuItems.forEach(item => {
const labelElement = item.querySelector('[class*="labelText--"]');
// 尝试找到具有 isSelected 类的元素
const selectedElement = item.querySelector('[class*="isSelected--"] [class*="valueItemText--"]') ||
item.querySelector('[class*="valueItem--"][class*="isSelected--"] [class*="valueItemText--"]');
if (labelElement && selectedElement) {
const label = labelElement.textContent.trim();
const value = selectedElement.textContent.trim();
productInfo.specs[label] = value;
}
});
}
return productInfo;
}
// 保存商品到本地
function saveProduct(info) {
let list = GM_getValue('productList', []);
// 避免重复(用链接去重)
if (list.some(item => item.url === info.url)) {
showPageNotification('已存在', '该商品已保存过', 'info');
return;
}
list.push(info);
GM_setValue('productList', list);
showPageNotification('保存成功', '商品已加入批量导出列表');
}
// 导出所有已保存商品
function exportAllProducts() {
let list = GM_getValue('productList', []);
if (!list.length) {
showPageNotification('无数据', '没有可导出的商品', 'info');
return;
}
const headers = ['序号', '商品名称', '规格', '价格', '数量', '总价', '链接'];
const rows = [headers];
list.forEach((item, idx) => {
const specsText = Object.entries(item.specs || {})
.map(([label, value]) => `${label}:${value}`)
.join('/');
const price = parseFloat(item.price) || 0;
const quantity = item.quantity || 1;
const sum = price * quantity;
rows.push([
idx + 1,
item.title,
specsText,
item.price,
quantity,
sum.toFixed(2),
item.url
]);
});
// 动态加载 SheetJS 并导出 Excel
function doExport() {
/* global XLSX */
const ws = XLSX.utils.aoa_to_sheet(rows);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "商品列表");
const wbout = XLSX.write(wb, {bookType: 'xlsx', type: 'array'});
const blob = new Blob([wbout], {type: "application/octet-stream"});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = `商品列表_${new Date().toLocaleDateString().replace(/\//g, '-')}.xlsx`;
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
showPageNotification('导出成功', `已导出${list.length}条商品`);
}
if (typeof XLSX === 'undefined') {
// 没有加载过SheetJS,动态加载
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/xlsx.full.min.js';
script.onload = doExport;
document.body.appendChild(script);
} else {
doExport();
}
}
// 清空已保存商品
function clearAllProducts() {
GM_setValue('productList', []);
showPageNotification('已清空', '商品列表已清空', 'success');
}
// 显示商品列表
function showProductList() {
// 先移除已有的浮窗
const old = document.getElementById('tm-goods-list-panel');
if (old) old.remove();
let list = GM_getValue('productList', []);
// 兼容老数据
list.forEach(item => {
if (typeof item.quantity !== 'number' || isNaN(item.quantity)) item.quantity = 1;
});
const panel = document.createElement('div');
panel.id = 'tm-goods-list-panel';
panel.style.position = 'fixed';
panel.style.top = '80px';
panel.style.left = '50%';
panel.style.transform = 'translateX(-50%)';
panel.style.zIndex = '100000';
panel.style.background = '#fff';
panel.style.border = '1px solid #ddd';
panel.style.borderRadius = '8px';
panel.style.boxShadow = '0 4px 16px rgba(0,0,0,0.15)';
panel.style.padding = '20px';
panel.style.minWidth = '700px';
panel.style.maxHeight = '70vh';
panel.style.overflowY = 'auto';
// 关闭按钮
const closeBtn = document.createElement('span');
closeBtn.textContent = '×';
closeBtn.style.position = 'absolute';
closeBtn.style.top = '10px';
closeBtn.style.right = '20px';
closeBtn.style.fontSize = '22px';
closeBtn.style.cursor = 'pointer';
closeBtn.onclick = () => panel.remove();
panel.appendChild(closeBtn);
// 标题
const title = document.createElement('div');
title.textContent = `已保存商品(${list.length})`;
title.style.fontWeight = 'bold';
title.style.marginBottom = '12px';
panel.appendChild(title);
// 表格
const table = document.createElement('table');
table.style.width = '100%';
table.style.borderCollapse = 'collapse';
table.innerHTML = `
<tr style="background:#f5f5f5;">
<th style="padding:6px;border:1px solid #eee;min-width:30px;">序号</th>
<th style="padding:6px;border:1px solid #eee;min-width:250px;">商品名称</th>
<th style="padding:6px;border:1px solid #eee;min-width:150px;">规格</th>
<th style="padding:6px;border:1px solid #eee;min-width:80px;">价格</th>
<th style="padding:6px;border:1px solid #eee;min-width:30px;">数量</th>
<th style="padding:6px;border:1px solid #eee;min-width:80px;">总价</th>
<th style="padding:6px;border:1px solid #eee;min-width:50px;">链接</th>
<th style="padding:6px;border:1px solid #eee;min-width:50px;">操作</th>
</tr>
`;
let totalSum = 0;
list.forEach((item, idx) => {
const specsText = Object.entries(item.specs || {})
.map(([label, value]) => `${label}:${value}`)
.join('/');
const price = parseFloat(item.price) || 0;
const quantity = item.quantity || 1;
const sum = price * quantity;
totalSum += sum;
const tr = document.createElement('tr');
tr.innerHTML = `
<td style="padding:6px;border:1px solid #eee;">${idx + 1}</td>
<td style="padding:6px;border:1px solid #eee;">${item.title}</td>
<td style="padding:6px;border:1px solid #eee;">${specsText}</td>
<td style="padding:6px;border:1px solid #eee;">${item.price}</td>
<td style="padding:6px;border:1px solid #eee;">
<input type="number" min="1" value="${quantity}" data-idx="${idx}" style="width:60px;">
</td>
<td style="padding:6px;border:1px solid #eee;" data-sum="sum">${sum.toFixed(2)}</td>
<td style="padding:6px;border:1px solid #eee;word-break:break-all;">
<a href="${item.url}" target="_blank" style="color:#2196F3;">链接</a>
</td>
<td style="padding:6px;border:1px solid #eee;">
<button data-idx="${idx}" data-action="copy" style="color:#fff;background:#2196F3;border:none;border-radius:3px;padding:2px 8px;cursor:pointer;margin-right:5px;">复制</button>
<button data-idx="${idx}" data-action="delete" style="color:#fff;background:#F44336;border:none;border-radius:3px;padding:2px 8px;cursor:pointer;">删除</button>
</td>
`;
table.appendChild(tr);
});
panel.appendChild(table);
// 总价显示
const totalDiv = document.createElement('div');
totalDiv.id = 'tm-goods-total-sum';
totalDiv.style.marginTop = '16px';
totalDiv.style.fontWeight = 'bold';
totalDiv.style.fontSize = '16px';
totalDiv.textContent = `总价合计:${totalSum.toFixed(2)}`;
panel.appendChild(totalDiv);
// 事件处理
panel.addEventListener('input', function(e) {
if (e.target.tagName === 'INPUT' && e.target.type === 'number' && e.target.dataset.idx) {
let idx = Number(e.target.dataset.idx);
let val = parseInt(e.target.value, 10);
if (isNaN(val) || val < 1) val = 1;
e.target.value = val;
list[idx].quantity = val;
GM_setValue('productList', list);
// 更新总价
const price = parseFloat(list[idx].price) || 0;
const sum = price * val;
// 更新当前行的总价
e.target.parentElement.parentElement.querySelector('[data-sum="sum"]').textContent = sum.toFixed(2);
// 重新计算总价合计
let total = 0;
list.forEach(item => {
total += (parseFloat(item.price) || 0) * (item.quantity || 1);
});
totalDiv.textContent = `总价合计:${total.toFixed(2)}`;
}
});
panel.addEventListener('click', function(e) {
if (e.target.tagName === 'BUTTON' && e.target.dataset.idx) {
let idx = Number(e.target.dataset.idx);
if (e.target.dataset.action === 'delete') {
list.splice(idx, 1);
GM_setValue('productList', list);
showPageNotification('已删除', '商品已从列表移除', 'success');
panel.remove();
showProductList();
} else if (e.target.dataset.action === 'copy') {
const item = list[idx];
const specsText = Object.entries(item.specs || {})
.map(([label, value]) => `${label}:${value}`)
.join('/');
const text = `${item.title}\t${specsText}\t${item.price}\t${item.url}`;
GM_setClipboard(text);
showPageNotification('复制成功', '商品信息已复制到剪贴板', 'success');
}
}
});
document.body.appendChild(panel);
}
// 添加按钮
function addButtons() {
if (document.getElementById('tm-goods-copy-btn')) return;
// 复制商品按钮
const copyBtn = document.createElement('button');
copyBtn.id = 'tm-goods-copy-current-btn';
copyBtn.textContent = '复制商品';
copyBtn.style.position = 'fixed';
copyBtn.style.top = '80px';
copyBtn.style.right = '30px';
copyBtn.style.zIndex = '99999';
copyBtn.style.background = '#2196F3';
copyBtn.style.color = '#fff';
copyBtn.style.border = 'none';
copyBtn.style.borderRadius = '4px';
copyBtn.style.padding = '10px 18px';
copyBtn.style.fontSize = '16px';
copyBtn.style.cursor = 'pointer';
copyBtn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
copyBtn.addEventListener('click', function() {
const info = extractProductInfo();
if (!info.title || !info.url) {
showPageNotification('复制失败', '未能正确获取商品信息', 'error');
return;
}
const specsText = Object.entries(info.specs || {})
.map(([label, value]) => `${label}:${value}`)
.join('/');
const text = `${info.title}\t${specsText}\t${info.price}\t${info.url}`;
GM_setClipboard(text);
showPageNotification('复制成功', '商品信息已复制到剪贴板', 'success');
});
document.body.appendChild(copyBtn);
// 保存按钮
const saveBtn = document.createElement('button');
saveBtn.id = 'tm-goods-copy-btn';
saveBtn.textContent = '保存商品';
saveBtn.style.position = 'fixed';
saveBtn.style.top = '130px';
saveBtn.style.right = '30px';
saveBtn.style.zIndex = '99999';
saveBtn.style.background = '#ff6b81';
saveBtn.style.color = '#fff';
saveBtn.style.border = 'none';
saveBtn.style.borderRadius = '4px';
saveBtn.style.padding = '10px 18px';
saveBtn.style.fontSize = '16px';
saveBtn.style.cursor = 'pointer';
saveBtn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
saveBtn.addEventListener('click', function() {
const info = extractProductInfo();
if (!info.title || !info.url) {
showPageNotification('保存失败', '未能正确获取商品信息', 'error');
return;
}
saveProduct(info);
});
document.body.appendChild(saveBtn);
// 查看列表按钮
const listBtn = document.createElement('button');
listBtn.id = 'tm-goods-list-btn';
listBtn.textContent = '查看列表';
listBtn.style.position = 'fixed';
listBtn.style.top = '180px';
listBtn.style.right = '30px';
listBtn.style.zIndex = '99999';
listBtn.style.background = '#2196F3';
listBtn.style.color = '#fff';
listBtn.style.border = 'none';
listBtn.style.borderRadius = '4px';
listBtn.style.padding = '10px 18px';
listBtn.style.fontSize = '16px';
listBtn.style.cursor = 'pointer';
listBtn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
listBtn.addEventListener('click', showProductList);
document.body.appendChild(listBtn);
// 批量导出按钮
const exportBtn = document.createElement('button');
exportBtn.id = 'tm-goods-export-btn';
exportBtn.textContent = '批量导出';
exportBtn.style.position = 'fixed';
exportBtn.style.top = '230px';
exportBtn.style.right = '30px';
exportBtn.style.zIndex = '99999';
exportBtn.style.background = '#4CAF50';
exportBtn.style.color = '#fff';
exportBtn.style.border = 'none';
exportBtn.style.borderRadius = '4px';
exportBtn.style.padding = '10px 18px';
exportBtn.style.fontSize = '16px';
exportBtn.style.cursor = 'pointer';
exportBtn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
exportBtn.addEventListener('click', exportAllProducts);
document.body.appendChild(exportBtn);
// 清空按钮
const clearBtn = document.createElement('button');
clearBtn.id = 'tm-goods-clear-btn';
clearBtn.textContent = '清空列表';
clearBtn.style.position = 'fixed';
clearBtn.style.top = '280px';
clearBtn.style.right = '30px';
clearBtn.style.zIndex = '99999';
clearBtn.style.background = '#888';
clearBtn.style.color = '#fff';
clearBtn.style.border = 'none';
clearBtn.style.borderRadius = '4px';
clearBtn.style.padding = '10px 18px';
clearBtn.style.fontSize = '16px';
clearBtn.style.cursor = 'pointer';
clearBtn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
clearBtn.addEventListener('click', function() {
if (confirm('确定要清空所有已保存的商品吗?')) {
clearAllProducts();
}
});
document.body.appendChild(clearBtn);
}
// 页面加载后添加按钮
setTimeout(addButtons, 1000);
})();