// ==UserScript==
// @name 美团搬菜(支持超市水果店)
// @description 从美团将菜单导入到搬菜平台
// @version v2.0.1MAX
// @author mirari、ChengPP(后续)
// @copyright 2023, mirari (https://github.com/mirari)
// @match https://cactivityapi-sc.waimai.meituan.com/h5*
// @match https://h5.waimai.meituan.com/waimai/mindex*
// @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
// @namespace https://greasyfork.org/users/1436563
// ==/UserScript==
(function () {
'use strict';
let food;
let autoGetting = false;
let isImporting = false;
const createButtons = () => {
const mainBtn = document.createElement('button');
mainBtn.innerHTML = '<img src="https://himg.bdimg.com/sys/portrait/item/pp.1.61637635.q_9U7gFy_biR3yojcvZygw.jpg?tt=1732025929684" alt="☰" style="width: 30px; height: 30px;">';
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;
`;
const parseBtn = document.createElement('button');
parseBtn.innerHTML = '解析新结构分类';
parseBtn.style = `
position: fixed;
top: -999vw;
right: -300vw;
transition: right 0.3s ease-in-out, transform 0.3s ease-in-out, background-color 0.3s ease-in-out, color 0.3s ease-in-out;
z-index: 999999;
background: #ffbd27;
border-radius: 1.6vw;
border: none;
padding: 1.5vw;
color: white;
font-weight: bold;
`;
const btnImport = document.createElement('button');
btnImport.innerHTML = '导入店铺菜单';
btnImport.style = `
position: fixed;
top: 7vw;
right: -300vw;
transition: right 0.3s ease-in-out, transform 0.3s ease-in-out, background-color 0.3s ease-in-out, color 0.3s ease-in-out;
z-index: 999999;
background: #ffbd27;
border-radius: 1.6vw;
border: none;
padding: 1.5vw;
color: white;
font-weight: bold;
`;
const btnShowRawData = document.createElement('button');
btnShowRawData.innerHTML = '获取原始数据';
btnShowRawData.style = `
position: fixed;
top: 12vw;
right: -300vw;
transition: right 0.3s ease-in-out, transform 0.3s ease-in-out, background-color 0.3s ease-in-out, color 0.3s ease-in-out;
z-index: 999999;
background: #ffbd27;
border-radius: 1.6vw;
border: none;
padding: 1.5vw;
color: white;
font-weight: bold;
`;
const btnShowCategories = document.createElement('button');
btnShowCategories.innerHTML = '获取商品状态';
btnShowCategories.style = `
position: fixed;
top: 17vw;
right: -300vw;
transition: right 0.3s ease-in-out, transform 0.3s ease-in-out, background-color 0.3s ease-in-out, color 0.3s ease-in-out;
z-index: 999999;
background: #ffbd27;
border-radius: 1.6vw;
border: none;
padding: 1.5vw;
color: white;
font-weight: bold;
`;
return { mainBtn, parseBtn, btnImport, btnShowRawData, btnShowCategories };
};
const { mainBtn, parseBtn, btnImport, btnShowRawData, btnShowCategories } = createButtons();
let isExpanded = false;
mainBtn.onclick = () => {
if (isExpanded) {
parseBtn.style.right = '-300vw';
btnImport.style.right = '-300vw';
btnShowRawData.style.right = '-300vw';
btnShowCategories.style.right = '-300vw';
} else {
parseBtn.style.right = '2vw';
btnImport.style.right = '2vw';
btnShowRawData.style.right = '2vw';
btnShowCategories.style.right = '2vw';
}
isExpanded = !isExpanded;
mainBtn.style.transform = isExpanded ? 'scale(1.2)' : 'scale(1)';
};
parseBtn.onclick = async () => {
if (window.location.href.startsWith('https://cactivityapi-sc.waimai.meituan.com/h5/sub-trade/restaurant/restaurant?')) {
parseBtn.style.backgroundColor = '#ff8000';
parseBtn.style.color = 'white';
parseBtn.innerHTML = '解析中...';
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟解析过程
alert('解析新结构分类成功...');
location.reload();
} else {
alert('当前页面不需要解析新结构分类。');
}
};
btnImport.onclick = () => {
if (food) {
const tagCount = food.data.food_spu_tags.length;
const spuCount = food.data.food_spu_tags.reduce((sum, tag) => sum + tag.spus.length, 0);
console.log(food);
console.log('tagCount', tagCount);
console.log('spuCount', spuCount);
const incompleteTags = food.data.food_spu_tags.filter(tag => !tag.spus.length && tag.attempted !== true);
const noProductTags = food.data.food_spu_tags.filter(tag => !tag.spus.length && tag.attempted === true);
const incompleteTagCount = incompleteTags.length;
const noProductTagCount = noProductTags.length;
let tip = '';
if (incompleteTagCount > 0 || noProductTagCount > 0) {
if (window.location.href.includes('https://h5.waimai.meituan.com/waimai/mindex/menu?')) {
tip = `🌟Tip1:当前为分页菜单,请点击黄色的标签分类获取商品。\n🌟Tip2:当店铺商品数较多时候,请浏览完整商品页面,避免获取缺失!!!`;
} else if (window.location.href.includes('https://cactivityapi-sc.waimai.meituan.com/h5') ) {
tip = `🌟Tip1:商品获取情况,可用查看已获取分类按钮查看详情...\n🌟Tip2:新结构店铺商品数较多,请浏览完整商品页面,避免获取缺失!!!`;
}
}
let confirmMessage = ``;
if (tip) {
confirmMessage += `${tip}\n\n`;
}
confirmMessage += `获取到分类${tagCount}个,商品共计${spuCount}个(存在重复计入)。\n`;
if (incompleteTagCount > 0) {
confirmMessage += `注意:当前还有${incompleteTagCount}个分类未获取完整信息。\n`;
}
if (noProductTagCount > 0) {
confirmMessage += `注意:当前有${noProductTagCount}个分类无商品。\n`;
}
confirmMessage += `是否导入到搬店平台?`;
if (confirm(confirmMessage)) {
btnImport.style.transform = 'scale(1.2)';
btnImport.style.backgroundColor = '#ff8000';
btnImport.style.color = 'white';
btnImport.innerHTML = '正在导入中...';
importData();
}
} else {
alert('未能监听到菜单数据。');
}
};
btnShowRawData.onclick = () => {
if (food) {
const allFoodData = JSON.stringify(food, null, 2);
const blob = new Blob([allFoodData], { type: 'application/json' });
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
} else {
alert('未能监听到菜单数据。');
}
};
btnShowCategories.onclick = () => {
if (food) {
const completeTags = food.data.food_spu_tags.filter(tag => tag.spus.length);
const completeTagCount = completeTags.length;
const totalSpuCount = completeTags.reduce((sum, tag) => sum + tag.spus.length, 0);
const incompleteTags = food.data.food_spu_tags.filter(tag => !tag.spus.length && tag.attempted !== true);
const incompleteTagCount = incompleteTags.length;
const incompleteTagNames = incompleteTags.map(tag => tag.name).join(', ');
const noProductTags = food.data.food_spu_tags.filter(tag => !tag.spus.length && tag.attempted === true);
const noProductTagCount = noProductTags.length;
const noProductTagNames = noProductTags.map(tag => tag.name).join(', ');
const totalTagCount = food.data.food_spu_tags.length;
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;
`;
const statsContainer = document.createElement('div');
statsContainer.style = `
margin-bottom: 10px;
padding: 10px;
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 5px;
`;
const statsText = `
<p>总分类数: ${totalTagCount}</p>
<p>已获取分类数: ${completeTagCount}</p>
<p>未获取分类数: ${incompleteTagCount}</p>
<p>无商品分类数: ${noProductTagCount}</p>
<p>已获取商品数: ${totalSpuCount}</p>
`;
statsContainer.innerHTML = statsText;
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);
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);
};
filterContainer.appendChild(closeButton);
const table = document.createElement('table');
table.style = 'width: 100%; border-collapse: collapse;';
const thead = document.createElement('thead');
thead.innerHTML = `
<tr>
<th style="border: 1px solid black; padding: 8px;">分类名称</th>
<th style="border: 1px solid black; padding: 8px;">商品数量</th>
<th style="border: 1px solid black; padding: 8px;">获取状态</th>
</tr>
`;
const tbody = document.createElement('tbody');
food.data.food_spu_tags.forEach(tag => {
let status;
if (!tag.spus.length && tag.attempted === true) {
status = '无商品';
} else if (!tag.spus.length) {
status = '未获取';
} else {
status = '已获取';
}
const statusColor = {
'已获取': '#4caf50',
'未获取': '#f44336',
'无商品': '#FFA500',
'失败': '#ff9800'
}[status] || '#000';
tbody.innerHTML += `
<tr>
<td style="border: 1px solid black; padding: 8px;">${tag.name}</td>
<td style="border: 1px solid black; padding: 8px; text-align: right;">${tag.spus.length}</td>
<td style="border: 1px solid black; padding: 8px; color: ${statusColor};">${status}</td>
</tr>
`;
});
table.appendChild(thead);
table.appendChild(tbody);
container.appendChild(statsContainer);
container.appendChild(filterContainer);
container.appendChild(table);
document.body.appendChild(container);
const updateTable = (filterValue) => {
const rows = tbody.getElementsByTagName('tr');
Array.from(rows).forEach(row => {
const statusCell = row.cells[2].innerText;
if (filterValue === '全部' || statusCell === filterValue) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
};
updateTable(filterSelect.value);
} else {
alert('未能监听到菜单数据。');
}
};
const importData = () => {
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.style.transform = 'scale(1)';
btnImport.style.backgroundColor = '#ffbd27';
btnImport.style.color = 'white';
btnImport.innerHTML = '导入店铺菜单';
}
};
const data = JSON.stringify({
raw: JSON.stringify(food),
});
xhr.send(data);
} catch (err) {
console.log(err);
alert('请求出错:' + err.message);
btnImport.style.transform = 'scale(1)';
btnImport.style.backgroundColor = '#ffbd27';
btnImport.style.color = 'white';
btnImport.innerHTML = '导入店铺菜单';
}
};
const originFetch = fetch;
window.unsafeWindow.fetch = (url, options) => {
return originFetch(url, options).then(async response => {
if (response.status === 403) {
alert('请切换美团账号或者清除浏览器缓存重试/页面拒绝访问!');
return response;
}
const callback = checkRequest(url);
if (callback) {
callback(url, await response.clone().json());
}
return response;
});
};
const originOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (_, url) {
const callback = checkRequest(url);
if (callback) {
this.addEventListener('readystatechange', function () {
if (this.readyState === 4) {
if (this.status === 403) {
alert('请切换美团账号或者清除浏览器缓存重试/页面拒绝访问!');
return;
}
callback(url, JSON.parse(this.responseText));
}
});
}
originOpen.apply(this, arguments);
};
function checkRequest(url) {
if (url.startsWith('https://i.waimai.meituan.com/openapi/v1/poi/food?') || url.startsWith('https://wx-shangou.meituan.com/quickbuy/v1/poi/food?')) {
return onGetStoreMenu;
} else if (url.startsWith('https://i.waimai.meituan.com/openh5/v2/poi/menuproducts?')) {
return onGetPaginatedMenuProducts;
} else if (url.startsWith('https://wx-shangou.meituan.com/quickbuy/v1/poi/sputag/products?') || url.startsWith('https://wx-shangou.meituan.com/quickbuy/v1/poi/product/smooth/render?')) {
return onGetNewStructureMenuProducts;
}
}
function onGetStoreMenu(url, res) {
food = res;
const tags = food.data.food_spu_tags;
if (tags.length) {
tags.forEach(tag => {
if (tag.tags && tag.tags.length) {
tag.spus = [];
tag.tags.forEach(subTag => {
if (subTag.spus && subTag.spus.length) {
tag.spus.push(...subTag.spus);
}
});
}
});
document.body.appendChild(mainBtn);
document.body.appendChild(parseBtn);
document.body.appendChild(btnImport);
document.body.appendChild(btnShowRawData);
document.body.appendChild(btnShowCategories);
refreshTabStatus();
}
}
function onGetPaginatedMenuProducts(url, res) {
const tags = food.data.food_spu_tags;
const tagId = res.data.product_tag_id;
const currentTag = tags.find(tag => tag.tag === tagId);
if (currentTag) {
currentTag.spus = [...new Set([...currentTag.spus, ...res.data.product_spu_list])]; // 确保唯一性
currentTag.attempted = true; // 标记该分类已被尝试获取
refreshTabStatus();
} else {
alert('未找到当前标签,请刷新后重试或切换美团账号后重试');
}
}
function onGetNewStructureMenuProducts(url, res) {
const tags = food.data.food_spu_tags;
const selectedCategoryName = getSelectedCategoryName();
const currentTag = tags.find(tag => tag.name === selectedCategoryName);
if (currentTag) {
currentTag.spus = [...new Set([...currentTag.spus, ...res.data.product_spu_list])]; // 确保唯一性
currentTag.attempted = true; // 标记该分类已被尝试获取
} else {
console.warn(`未找到当前标签`);
alert('获取到商品数据但是匹配标签失败!!!\n1. 可尝试刷新界面重新获取\n2. 进行手动归类');
const panel = document.createElement('div');
panel.style = `
position: fixed;
top: 15vw;
right: 2vw;
z-index: 1000001;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-radius: 1.6vw;
padding: 1.5vw;
width: 20vw;
max-width: 300px;
`;
const title = document.createElement('div');
title.textContent = '选择分类';
title.style = `
font-size: 1.2em;
margin-bottom: 1vw;
font-weight: bold;
`;
const select = document.createElement('select');
select.style = `
width: 100%;
padding: 0.5vw;
margin-bottom: 1vw;
border: 1px solid #ccc;
border-radius: 0.5vw;
`;
const newOption = document.createElement('option');
newOption.value = 'new';
newOption.textContent = '新建自定义标签';
select.appendChild(newOption);
tags.forEach(tag => {
const option = document.createElement('option');
option.value = tag.name;
option.textContent = tag.name;
select.appendChild(option);
});
const input = document.createElement('input');
input.type = 'text';
input.placeholder = '输入新标签名称';
input.style = `
width: 100%;
padding: 0.5vw;
margin-bottom: 1vw;
border: 1px solid #ccc;
border-radius: 0.5vw;
display: block;
`;
select.onchange = () => {
if (select.value === 'new') {
input.style.display = 'block';
} else {
input.style.display = 'none';
}
};
const saveButton = document.createElement('button');
saveButton.textContent = '保存';
saveButton.style = `
width: 100%;
padding: 0.5vw;
background-color: #ffbd27;
color: white;
border: none;
border-radius: 0.5vw;
cursor: pointer;
`;
saveButton.onclick = () => {
let tagName;
if (select.value === 'new') {
tagName = input.value.trim();
if (!tagName) {
alert('请输入有效的标签名称');
return;
}
} else {
tagName = select.value;
}
const existingTag = tags.find(tag => tag.name.toLowerCase() === tagName.toLowerCase());
if (existingTag) {
existingTag.spus = [...new Set([...existingTag.spus, ...res.data.product_spu_list])]; // 确保唯一性
existingTag.attempted = true; // 标记该分类已被尝试获取
} else {
const newTag = {
tag: tagName,
name: tagName,
spus: res.data.product_spu_list,
attempted: true
};
food.data.food_spu_tags.push(newTag);
}
document.body.removeChild(panel);
refreshTabStatus();
};
panel.appendChild(title);
panel.appendChild(select);
panel.appendChild(input);
panel.appendChild(saveButton);
document.body.appendChild(panel);
}
}
function refreshTabStatus() {
const navEl = document.querySelector("#sqt-openh5-menulist > [class^='root_'] > [class^='panel_'] > [class^='root_'] > [class^='root_']");
if (navEl) {
for (let i = 0; i < navEl.children.length; i++) {
navEl.children[i].style.backgroundColor = '#CCE099'; // 获取成功设置为绿色
}
food.data.food_spu_tags.forEach((item, index) => {
if (!item.spus.length && item.attempted !== true) {
const btnTab = findElementWithInnerText(navEl, item.name);
if (btnTab) {
btnTab.style.backgroundColor = 'yellow'; // 设置背景颜色为黄色
}
} else if (!item.spus.length && item.attempted === true) {
const btnTab = findElementWithInnerText(navEl, item.name);
if (btnTab) {
btnTab.style.backgroundColor = '#FFA500'; // 设置背景颜色为橙色
}
}
});
}
}
function findElementWithInnerText(el, text) {
for (let i = 0; i < el.children.length; i++) {
if (el.children[i].innerText.trim() === text) {
return el.children[i];
}
}
return null;
}
function getSelectedCategoryName() {
const activeCategory = document.querySelector('.category-cat-item-name.category-active-type-one');
if (activeCategory) {
return activeCategory.querySelector('.category-cat-item-text').innerText.trim();
}
const mtViewCategory = document.querySelector('mt-view.p-left-sub-tab-title.mt-active');
if (mtViewCategory) {
const mtTextView = mtViewCategory.querySelector('mt-view.p-sub-tab-text');
if (mtTextView) {
return mtTextView.innerText.trim();
}
}
return null;
}
document.body.appendChild(mainBtn);
document.body.appendChild(btnImport);
document.body.appendChild(btnShowRawData);
document.body.appendChild(btnShowCategories);
// 添加按钮点击效果
[mainBtn, parseBtn, btnImport, btnShowRawData, btnShowCategories].forEach(btn => {
btn.addEventListener('mouseover', () => {
btn.style.transform = 'scale(1.2)';
});
btn.addEventListener('mouseout', () => {
btn.style.transform = 'scale(1)';
});
});
// 检查是否是新结构菜单界面,并决定是否显示解析新结构分类按钮
if (window.location.href.startsWith('https://cactivityapi-sc.waimai.meituan.com/h5/sub-trade/restaurant/restaurant?')) {
document.body.appendChild(parseBtn);
// 调整其他按钮的位置以保持在同一垂直线上
parseBtn.style.top = '7vw';
btnImport.style.top = '12vw';
btnShowRawData.style.top = '17vw';
btnShowCategories.style.top = '22vw';
}
})();