// ==UserScript==
// @name 饿了么搬菜
// @description 抓取ele.me页面上的分类及商品信息并统计
// @version v1.0.1
// @author ChengPP
// @match https://h5.ele.me/*
// @run-at document-idle
// @grant unsafeWindow
// @grant GM_xmlhttpRequest
// @connect raw.githubusercontent.com
// @connect mv.nianxiang.net.cn
// @connect localhost
// @connect *
// @icon https://himg.bdimg.com/sys/portrait/item/pp.1.61637635.q_9U7gFy_biR3yojcvZygw.jpg?tt=1732025929684
// @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.5/xlsx.full.min.js
// @namespace https://greasyfork.org/users/1436563
// @downloadURL https://update.greasyfork.icu/scripts/527331/%E9%A5%BF%E4%BA%86%E4%B9%88%E6%90%AC%E8%8F%9C.user.js
// @updateURL https://update.greasyfork.icu/scripts/527331/%E9%A5%BF%E4%BA%86%E4%B9%88%E6%90%AC%E8%8F%9C.meta.js
// ==/UserScript==
(function() {
'use strict';
let categories = {};
const specificationsStorage = {}; // 使用一个对象来代替localStorage存储规格信息
let nextSpuId = 1; // 用于生成唯一的SPU ID
let food = null; // 存储最终的数据结构
let shopInfo = {}; // 存储门店信息
let poiIdStr = 'ZhengJinCheng'; // 默认值
const createMainButton = () => {
const mainBtn = document.createElement('button');
mainBtn.innerHTML = '';
mainBtn.style = `
position: fixed;
top: 2vw;
right: 2vw;
z-index: 999999;
background: transparent;
border: none;
padding: 0;
cursor: pointer;
transition: transform 0.3s ease-in-out, color 0.3s ease-in-out;
`;
return mainBtn;
};
const createImportButton = () => {
const btn = document.createElement('button');
btn.innerHTML = '导入到搬点平台';
btn.style = `
position: fixed;
top: 8vw;
right: -300vw;
width: 150px;
height: 40px;
z-index: 999999;
background: #4CAF50;
border: none;
border-radius: 5px;
color: white;
font-weight: bold;
cursor: pointer;
transition: opacity 0.3s ease-in-out;
opacity: 0;
`;
return btn;
};
const createParseButton = () => {
const btn = document.createElement('button');
btn.innerHTML = '获取商品状态';
btn.style = `
position: fixed;
top: 14vw;
right: -300vw;
width: 150px;
height: 40px;
z-index: 999999;
background: #ffbd27;
border: none;
border-radius: 5px;
color: white;
font-weight: bold;
cursor: pointer;
transition: opacity 0.3s ease-in-out;
opacity: 0;
`;
return btn;
};
const createDataConversionButton = () => {
const btn = document.createElement('button');
btn.innerHTML = '查看数据';
btn.style = `
position: fixed;
top: 20vw;
right: -300vw;
width: 150px;
height: 40px;
z-index: 999999;
background: #9E9E9E;
border: none;
border-radius: 5px;
color: white;
font-weight: bold;
cursor: pointer;
transition: opacity 0.3s ease-in-out;
opacity: 0;
`;
return btn;
};
const mainBtn = createMainButton();
const btnImport = createImportButton();
const parseBtn = createParseButton();
const btnDataConversion = createDataConversionButton();
document.body.appendChild(mainBtn);
document.body.appendChild(btnImport);
document.body.appendChild(parseBtn);
document.body.appendChild(btnDataConversion);
let isExpanded = false;
mainBtn.onclick = () => {
if (isExpanded) {
btnImport.style.right = '-300vw';
parseBtn.style.right = '-300vw';
btnDataConversion.style.right = '-300vw';
} else {
btnImport.style.right = '2vw';
parseBtn.style.right = '2vw';
btnDataConversion.style.right = '2vw';
btnImport.style.opacity = '1';
parseBtn.style.opacity = '1';
btnDataConversion.style.opacity = '1';
}
isExpanded = !isExpanded;
mainBtn.style.transform = isExpanded ? 'scale(1.2)' : 'scale(1)';
mainBtn.style.width = isExpanded ? '30px' : '15px';
mainBtn.style.height = isExpanded ? '30px' : '15px';
};
btnImport.onclick = () => {
if (Object.keys(categories).length > 0) {
const totalItems = Object.values(categories).reduce((sum, items) => sum + items.length, 0);
const emptyCategories = Object.entries(categories)
.filter(([id, items]) => items.length === 0)
.map(([id]) => categories[id][0]?.name || id)
.join(', ');
const message = emptyCategories ? `还有如下分类未获取:${emptyCategories}(共${emptyCategories.split(', ').length}个),${shopInfo.name}目前获取到商品${totalItems}个,是否继续导入?` : `${shopInfo.name}目前获取到商品${totalItems}个,是否确认导入到搬点平台?`;
if (confirm(message)) {
btnImport.innerHTML = '正在导入商品中...';
btnImport.style.backgroundColor = '#FF5722';
btnImport.style.transform = 'scale(1.2)';
convertData(); // 调用数据转换函数
let xhr = new XMLHttpRequest();
try {
xhr.open("POST", 'https://mv.nianxiang.net.cn/api/admin/move/task/open/import', true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
const res = JSON.parse(xhr.responseText);
console.log(res);
alert(res.data);
btnImport.innerHTML = '导入到搬点平台';
btnImport.style.backgroundColor = '#4CAF50';
btnImport.style.transform = 'scale(1)';
}
};
const data = JSON.stringify({
raw: JSON.stringify(food),
});
xhr.send(data);
} catch (err) {
console.log(err);
alert('请求出错:' + err.message);
btnImport.innerHTML = '导入到搬点平台';
btnImport.style.backgroundColor = '#4CAF50';
btnImport.style.transform = 'scale(1)';
}
}
} else {
alert('尚未获取到分类及商品信息,请先浏览页面加载相关信息。');
}
};
parseBtn.onclick = () => {
if (Object.keys(categories).length > 0) {
importSpecifications();
showStatisticsTable();
} else {
getCategoryData();
getMenuItemData();
getShopInfo();
getPoiIdStr();
parseBtn.innerHTML = '获取商品状态';
parseBtn.style.backgroundColor = '#ffbd27';
parseBtn.style.transform = 'scale(1)';
}
};
btnDataConversion.onclick = () => {
if (Object.keys(categories).length > 0) {
convertDataAndShow();
} else {
alert('尚未获取到分类及商品信息,请先浏览页面加载相关信息。');
}
};
const getCategoryData = () => {
const categoryElements = document.querySelectorAll('.sideList--item .sideList--item-text');
categories = {};
categoryElements.forEach(element => {
const categoryName = element.textContent.trim();
categories[categoryName] = [];
});
console.log('分类数据获取完成:', categories);
};
const getMenuItemData = () => {
const menuItemElements = document.querySelectorAll('[data-cate-name]');
menuItemElements.forEach(element => {
const categoryName = element.getAttribute('data-cate-name');
const fractionElement = element.querySelector('.menuItem--info-price-text.fraction');
const fraction = fractionElement ? parseFloat(fractionElement.textContent) : 0;
const item = {
id: nextSpuId++,
name: element.getAttribute('data-food-detail-title'),
min_price: parseFloat(element.querySelector('.menuItem--info-price-text')?.textContent.replace('¥', '')) + (fraction ? fraction : 0),
picture: element.getAttribute('data-food-detail-img'),
attrs: specificationsStorage[element.getAttribute('data-food-detail-title')] || [],
sku_label: element.querySelector('.menuItem--info-price .menuItem--info-price-text')?.textContent || '规格',
skus: []
};
if (categories[categoryName]) {
categories[categoryName].push(item);
}
});
console.log('商品数据获取完成:', categories);
};
const importSpecifications = () => {
const popupElements = document.querySelectorAll('.sku__wrapper, .sku-wrapper');
popupElements.forEach(popupElement => {
const itemName = popupElement.querySelector('.sku--header-title, .sku-header .sku-header-title')?.textContent?.trim();
const specGroups = popupElement.querySelectorAll('.sku--group, .sku-group');
const specifications = [];
specGroups.forEach(group => {
const specTitle = group.querySelector('.sku--body_h2, .sku-group-title')?.textContent?.trim();
const options = Array.from(group.querySelectorAll('.sku-option__root, .sku-group-item')).map(optionElement => {
const nameSelector = optionElement.querySelector('.ml-ellipsis.lh-32.font-24.color-19, .sku-group-item-title, .lh-30');
const priceSelector = optionElement.querySelector('.option-price, .sku-group-item-price');
return {
name: nameSelector?.textContent?.trim(),
price: priceSelector?.textContent || '+ ¥0'
};
});
specifications.push({
name: specTitle,
values: options.map(option => ({
id: option.name, // 这里假设每个选项的名字是唯一的,可以用作ID
value: option.name
}))
});
});
specificationsStorage[itemName] = specifications; // 将规格信息存储在内存中
let matched = false;
Object.values(categories).forEach(categoryItems => {
const item = categoryItems.find(item => item.name === itemName);
if (item && !item.specsImported) {
item.attrs = item.attrs.concat(specifications);
item.specsImported = true;
matched = true;
const specInfo = JSON.stringify(specifications, null, 2);
console.log(`商品 "${itemName}" 的规格信息导入完成:\n${specInfo}`);
}
});
if (!matched) {
console.log(`未找到与商品 "${itemName}" 匹配的信息`);
}
});
};
const showStatisticsTable = () =>
{
const container = document.createElement('div');
container.style = `
max-height: 80vh;
overflow-y: auto;
padding: 10px;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
z-index: 1000000;
position: fixed;
top: 15vw;
left: 50%;
transform: translateX(-50%);
width: 80vw;
max-width: 600px;
border-radius: 1.6vw;
`;
// 获取店铺名称和logo
const shopNameElementNew = document.querySelector('.shop-header-info-card__name.mor-comp-view');
const shopLogoElementNew = document.querySelector('.shop-header-info-card__logo.mor-comp-view');
const shopNameElementOld = document.getElementById('shic2-shop-name');
const shopLogoElementOld = document.querySelector('.shic2-shop-logo');
let shopName = '';
let shopLogoUrl = '';
if (shopNameElementNew && shopLogoElementNew) {
shopName = shopNameElementNew.textContent.trim().replace(/&/g, '&');
shopLogoUrl = shopLogoElementNew.style.backgroundImage.replace(/url\(|\)|"/g, '') ;
} else if (shopNameElementOld && shopLogoElementOld) {
shopName = shopNameElementOld.textContent.trim().replace(/&/g, '&');
shopLogoUrl = shopLogoElementOld.getAttribute('src') || 'https://himg.bdimg.com/sys/portrait/item/pp.1.61637635.q_9U7gFy_biR3yojcvZygw.jpg?tt=1732025929684';
} else {
shopName = '未知店铺';
shopLogoUrl = 'https://himg.bdimg.com/sys/portrait/item/pp.1.61637635.q_9U7gFy_biR3yojcvZygw.jpg?tt=1732025929684';
}
const headerContainer = document.createElement('div');
headerContainer.style = `
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
`;
const logoImg = document.createElement('img');
logoImg.src = shopLogoUrl;
logoImg.alt = '店铺logo';
logoImg.style = `
width: 50px;
height: 50px;
object-fit: cover;
margin-right: 10px;
border-radius: 5px;
`;
const shopNameSpan = document.createElement('span');
shopNameSpan.innerText = shopName;
shopNameSpan.style = `
font-size: 16px;
font-weight: bold;
`;
headerContainer.appendChild(logoImg);
headerContainer.appendChild(shopNameSpan);
const buttonContainer = document.createElement('div');
buttonContainer.style = `
display: flex;
gap: 10px;
align-items: center;
`;
const closeButton = document.createElement('button');
closeButton.innerHTML = '关闭';
closeButton.style = `
padding: 5px 10px;
background-color: #ffbd27;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
`;
closeButton.onclick = () => {
document.body.removeChild(container);
};
const exportExcelButton = document.createElement('button');
exportExcelButton.innerHTML = '导出为Excel';
exportExcelButton.style = `
margin-right: 10px;
padding: 5px 10px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
`;
exportExcelButton.onclick = () => {
convertData();
if (!food || !food.data) {
alert('没有可用的商品数据!');
return;
}
exportToExcel(food);
};
const viewOnlineButton = document.createElement('button');
viewOnlineButton.innerHTML = '查看商品详细';
viewOnlineButton.style = `
padding: 5px 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
`;
viewOnlineButton.onclick = () => {
convertData();
if (!food || !food.data) {
alert('没有可用的商品数据!');
return;
}
openViewPage(food);
};
buttonContainer.appendChild(viewOnlineButton);
buttonContainer.appendChild(exportExcelButton);
buttonContainer.appendChild(closeButton);
headerContainer.appendChild(buttonContainer);
container.appendChild(headerContainer);
const statsContainer = document.createElement('div');
statsContainer.style = `
margin-bottom: 10px;
padding: 10px;
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 5px;
`;
const totalCategories = Object.keys(categories).length;
const totalItems = Object.values(categories).reduce((sum, items) => sum + items.length, 0);
const emptyCategories = Object.entries(categories)
.filter(([id, items]) => items.length === 0)
.map(([id]) => categories[id][0]?.name || id)
.length;
const statsText = `
总分类数: ${totalCategories}
已获取分类数: ${totalCategories - emptyCategories}
未获取分类数: ${emptyCategories}
已获取商品数: ${totalItems}
`; statsContainer.innerHTML = statsText; container.appendChild(statsContainer); const filterContainer = document.createElement('div'); filterContainer.style = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; `; const filterOptions = ['全部', '已获取', '未获取']; const filterSelect = document.createElement('select'); filterSelect.style = ` padding: 5px; border: 1px solid #ccc; border-radius: 5px; `; filterOptions.forEach(option => { const opt = document.createElement('option'); opt.value = option; opt.text = option; filterSelect.appendChild(opt); }); filterSelect.onchange = () => { updateTable(filterSelect.value); }; filterContainer.appendChild(filterSelect); container.appendChild(filterContainer); const table = document.createElement('table'); table.style = 'width: 100%; border-collapse: collapse;'; const thead = document.createElement('thead'); thead.innerHTML = `${JSON.stringify(food, null, 2)}`); }; const convertData = () => { food = { data: { food_spu_tags: [], poi_info: { id: -100, poi_id_str: poiIdStr, name: shopInfo.name, bulletin: "恭喜发财666", pic_url: shopInfo.logo } } }; // 添加分类信息 Object.keys(categories).forEach((categoryName, index) => { const category = { category_code: null, tag: `${index + 1}`, name: categoryName, spus: [] }; categories[categoryName].forEach(item => { const spu = { id: item.id, name: item.name, min_price: item.min_price, picture: item.picture, attrs: item.attrs, sku_label: item.sku_label, skus: item.skus.map(sku => ({ id: sku.id, spec: sku.spec })) }; category.spus.push(spu); }); food.data.food_spu_tags.push(category); }); console.log('数据转换完成:', food); }; /*---------------------------------------------------------------------------------------------------------------------------------*/ const getShopInfo = () => { // 尝试通过新的结构获取店铺名称和logo const shopNameElementNew = document.querySelector('.shop-header-info-card__name.mor-comp-view'); const shopLogoElementNew = document.querySelector('.shop-header-info-card__logo.mor-comp-view'); // 尝试通过旧的结构获取店铺名称和logo const shopNameElementOld = document.getElementById('shic2-shop-name'); const shopLogoElementOld = document.querySelector('.shic2-shop-logo'); let shopName = ''; let shopLogoUrl = ''; if (shopNameElementNew && shopLogoElementNew) { shopName = shopNameElementNew.textContent.trim().replace(/&/g, '&'); shopLogoUrl = shopLogoElementNew.style.backgroundImage.replace(/url\(|\)|"/g, '') ; } else if (shopNameElementOld && shopLogoElementOld) { shopName = shopNameElementOld.textContent.trim().replace(/&/g, '&'); shopLogoUrl = shopLogoElementOld.getAttribute('src') || 'https://himg.bdimg.com/sys/portrait/item/pp.1.61637635.q_9U7gFy_biR3yojcvZygw.jpg?tt=1732025929684'; } else { shopName = '未知店铺'; shopLogoUrl = 'https://himg.bdimg.com/sys/portrait/item/pp.1.61637635.q_9U7gFy_biR3yojcvZygw.jpg?tt=1732025929684'; } shopInfo = { name: shopName, logo: shopLogoUrl }; console.log('门店信息获取完成:', shopInfo); }; /*---------------------------------------------------------------------------------------------------------------------------------*/ const getPoiIdStr = () => { const urlParams = new URLSearchParams(window.location.search); const traceId = urlParams.get('trace_id'); if (traceId) { poiIdStr = traceId; } console.log('poi_id_str 获取完成:', poiIdStr); }; const observePopup = () => { const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.addedNodes.length > 0) { mutation.addedNodes.forEach(node => { if (node.classList && (node.classList.contains('sku__wrapper') || node.classList.contains('sku-wrapper'))) { const popupElement = node; const itemName = popupElement.querySelector('.sku--header-title, .sku-header .sku-header-title')?.textContent?.trim(); const specGroups = popupElement.querySelectorAll('.sku--group, .sku-group'); const specifications = []; specGroups.forEach(group => { const specTitle = group.querySelector('.sku--body_h2, .sku-group-title')?.textContent?.trim(); const options = Array.from(group.querySelectorAll('.sku-option__root, .sku-group-item')).map(optionElement => { const nameSelector = optionElement.querySelector('.ml-ellipsis.lh-32.font-24.color-19, .sku-group-item-title, .lh-30'); const priceSelector = optionElement.querySelector('.option-price, .sku-group-item-price'); return { name: nameSelector?.textContent?.trim(), price: priceSelector?.textContent || '+ ¥0' }; }); specifications.push({ name: specTitle, values: options.map(option => ({ id: option.name, // 这里假设每个选项的名字是唯一的,可以用作ID value: option.name })) }); }); specificationsStorage[itemName] = specifications; // 将规格信息存储在内存中 console.log(`规格信息已保存到内存中,商品名称: ${itemName}, 规格信息:`, specifications); } }); } }); }); observer.observe(document.body, { childList: true, subtree: true }); }; const observer = new MutationObserver(() => { if (window.location.href.includes('/2021001185671035/pages/ele-takeout-index/ele-takeout-index?')) { getCategoryData(); getMenuItemData(); getShopInfo(); getPoiIdStr(); } }); observer.observe(document.body, { childList: true, subtree: true }); // 初始化获取一次分类数据 if (window.location.href.includes('/2021001185671035/pages/ele-takeout-index/ele-takeout-index?')) { getCategoryData(); getMenuItemData(); getShopInfo(); getPoiIdStr(); } // 开始监听弹出页面 observePopup(); const exportToExcel = (foodData) => { if (!foodData || !foodData.data) { console.error('No valid food data provided to exportToExcel'); return; } const workbook = XLSX.utils.book_new(); const worksheetData = [ ['店名', '店铺logo', '店铺分类', '商品名称', '详细商品规格信息', '商品图片', '价格信息'] ]; foodData.data.poi_info.name = foodData.data.poi_info.name.replace(/"/g, '""'); // 处理包含双引号的情况 foodData.data.food_spu_tags.forEach(tag => { tag.spus.forEach(spu => { let specsInfo = ''; spu.attrs.forEach(attr => { specsInfo += `${attr.name}: ${attr.values.map(val => val.value).join('/')}\n`; }); specsInfo = specsInfo.trim().replace(/"/g, '""'); // 处理包含双引号的情况 worksheetData.push([ `"${foodData.data.poi_info.name}"`, foodData.data.poi_info.pic_url, tag.name, `"${spu.name}"`, specsInfo, spu.picture, spu.min_price ]); }); }); const worksheet = XLSX.utils.aoa_to_sheet(worksheetData); XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1'); XLSX.writeFile(workbook, `${foodData.data.poi_info.name}_商品信息.xlsx`); }; const openViewPage = (foodData) => { if (!foodData || !foodData.data) { console.error('No valid food data provided to openViewPage'); return; } const htmlContent = `
店名 | 店铺分类 | 商品名称 | 详细商品规格信息 | 商品图片 | 价格信息 |
---|---|---|---|---|---|
${foodData.data.poi_info.name} | ${tag.name} | ${spu.name} | ${spu.attrs.map(attr => `${attr.name}: ${attr.values.map(val => val.value).join('/')}`).join(' ')} |
${spu.min_price} |