Greasy Fork 支持简体中文。

美团搬菜(支持超市水果店)

从美团将菜单导入到搬菜平台

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