点点数据详细页完整版

在榜单详细页面加入候选, 并提供清单查看、删除和清空功能, 导出所有候选应用的 CSV 文件

// ==UserScript==
// @name         点点数据详细页完整版
// @version      2025-02-12
// @author       DethanZ
// @description  在榜单详细页面加入候选, 并提供清单查看、删除和清空功能, 导出所有候选应用的 CSV 文件
// @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD4AAAA+CAYAAABzwahEAAAE0klEQVRoBeXBPWjcZgCA4VefdGdHtppQixYfqJAhd0uXcEMhhpIETwZ5TJNs6eAbSungpgS6uNChNO7WDDGk2fKz2iVZjGsoXkqNl3a4yxCIQK7NXS6JfEp8sk7FgyEc+i4+6ZwE9DxKsxVFZJAgowQZJcgoQUYJMkqQUYKMEmSUIKMEGSXIKEFGaQzA39Vdfl/zed2oLrg8OUrBVDmwtOazXt1FZtzUMHSFopWjZOUxdIWjopGS2wipXG8QZ7MR8stXH7Jvac1n7vYz+lEu5bHP6NgTOoMmSKn2JOAwqk5Av9arbeZuP8O+tsXSms8gCVJyG3vIFK0cB2pOQFJuPWTu9jNmbzzFrYcMgiAltx4iUxhTOeD5EWmtbryiMl/HrYekJUip5gTIFEyVAzUnYBDcekhlvo7nR6QhOCIFU6Vo5dnn+R0Gya2HzN5okIbSbEURKbiNkKU1n27nTg9TtHIcWN14RdUJkPH8DjUnoOYEeH7EYcxdOYE9oZOE0mxFEe+ZpTWfhUUPtxHSi6ELln76GENX6JfgPWRP6Nz8zuTs6WF68fwOqxsvSUJptqKIhDw/4ubiC3b8Dq8b1QWV6Q8wdIW0Zm88ZXXjFTLlUp6Fqyb90kjh5uIL7i63iFOyctgTOmnNXTmBXd3C8yPirFfbeH6EoSv0Q5BCzQmQKZgqg2Dogplpg15qTpt+aaTg+REy42MacYbv3yK/8pB90cgo0cgo7XNTtM9PIWOf0VlY9PD8iDhVZ49yaYh+aKRQcwJkCqZKt/zKA4bv/UY37Z8NtH838L/+njiGLihaOdarbeJ4fod+CRJy6yEyRStHnNxffyKTX3nA8P1byBStHIMkSMht7CFj6ApxxPYmveRXHiJj6IJBEiRUcwJkilaOOOrjR/QitjdJwtAF/RIk5NZDZAqmRjexvcmbRCOjyNScNjKFMZV+CRLabOwhU7I0uont/3iT8OQpZGpP9pApWjn6JUjIrXeQGdVVuontTd6kfW6KODUnwG2ExCmYKgVTpV+ChDYbe8iULI1uYnuTXnbtC7TPTxHnzvIOMuXiEEkIEvD8Dp4fEacwphJHae0gs2tf4OWX3xDHbYQsrb1Exp44RhIaCVSdAJlxUyXOrn0B9fEjXheePEXw2efsfXoamcrPdWSKVo5yaYgkNBLY8SP61flonJ0ff6Uf8/ee4zZCZC5PjpCUIIGqEyBTc/YYhPl7z7m73ELm7Olh7AmdpAQJeH4HGc/vsF7dJSm3ETJzvc7d5RYyhq4w+8Vx0hAkYOiCXmZvNHHrIf3w/Iibix6Xf9hmvdqml8r0BxRMlTQ0EiiMqfTi+R3sa1vYZ45RLg0xbqp08/yIHb9D1QmoOQHr1TaHUZk2uDQ5QlpKsxVF9Mmth9jXtnjb7AmduSsnGARBAgVTpVzK8zZVpg3mrpxgUAQJVaYN3gZDV/j24nFmpg0GSZBQuTTE5ckRjlK5lGfhqsmlyREGTSOF2YvH2XdnucUgFUyVGdvAntA5KkqzFUWktLTms7Do4TZCkjJ0haKVozJtUC4NcdSUZiuKGJD16i5/bLyi5gQcRmFMpfhJjpKVo2jlMXSFt0VptqKIDBJklCCjBBklyChBRgkySpBRgowSZJQgowQZpfHuRbwD/wOX1rro0ceVGQAAAABJRU5ErkJggg==
// @match        https://app.diandian.com/rank/*
// @match        https://app.diandian.com/app/*
// @license      GPL-3.0 License
// @run-at       document-start
// @namespace    http://tampermonkey.net/
// @supportURL   https://github.com/dethanzhang/UserScript
// @homepageURL  https://github.com/dethanzhang/UserScript
// ==/UserScript==

(function () {
  "use strict";

  // 存储候选应用清单
  const candidates = JSON.parse(localStorage.getItem("candidates")) || [];

  // 备份原始的 open 和 send 方法
  const _open = XMLHttpRequest.prototype.open;
  const _send = XMLHttpRequest.prototype.send;

  // 重写 open 方法
  XMLHttpRequest.prototype.open = function (
    method,
    url,
    async,
    user,
    password
  ) {
    this._url = url; // 记录请求的 URL
    return _open.apply(this, arguments);
  };

  // 使用闭包来确保每个页面有独立的 captured 和 rankjson 变量
  (function () {
    let captured = false;
    let rankjson = [];

    const pattern = /\/trend\?.*&brand_id=0/; // 判断是否为指定的请求链接

    // 重写 send 方法
    XMLHttpRequest.prototype.send = function (body) {
      // 如果是符合条件的url则捕获
      if (!captured && pattern.test(this._url)) {
        let _onload = this.onload; // 备份原来的 onload 事件(如果有)
        this.onload = function (event) {
          if (_onload) _onload.call(this, event); // 保持原来的逻辑
          rankjson.push(JSON.parse(this.responseText));
          captured = true; // 处理完后设置标志为 true,停止进一步捕获
        };
      }
      return _send.apply(this, arguments);
    };

    // **添加应用到候选清单&存储所有数据的逻辑**
    async function addToCandidates() {
      if (!captured) {
        clickBtn();
      }
      // 等待 captured 变为 true
      while (!captured) {
        await new Promise((resolve) => setTimeout(resolve, 500)); // 等待
      }

      const { rank1, rank2, rank3 } = processJsonData(rankjson[0].data.list); // 处理jsonData

      const appName = document.querySelector("div.ellip.font-600")
        ? document.querySelector("div.ellip.font-600").innerText.trim()
        : "未知";
      const { appCategory, appDownloads } = getCategoryAndDownloads(); // 应用类别和下载量
      const appLink = window.location.href.replace(
        "/googleplay-rank?",
        "/googleplay?"
      );

      // 将当前应用的信息加入候选清单
      const appData = {
        name: appName,
        category: appCategory,
        rank1: rank1,
        rank2: rank2,
        rank3: rank3,
        downloads: formatNumberToChinese(appDownloads),
        pubtime: getPubTime(),
        link: appLink,
      };
      if (!candidates.some((app) => app.link === appData.link)) {
        candidates.push(appData); // 确保去重
        localStorage.setItem("candidates", JSON.stringify(candidates));
        // alert('已加入候选清单!');
        renderCandidatesPanel(); // 更新候选清单面板
      } else {
        alert("此应用已在候选清单中!");
      }
    }

    // 详情页显示“加入候选”
    if (window.location.href.includes("/app/")) {
      createButton("加入候选", addToCandidates);
    }
  })();

  // **获取当前日期(格式:YYYYMMDD)**
  function getCurrentDate() {
    const now = new Date();
    const year = now.getFullYear();
    const month = String(now.getMonth() + 1).padStart(2, "0"); // 月份是从 0 开始的
    const day = String(now.getDate()).padStart(2, "0");
    return `${year}${month}${day}`;
  }

  // **格式化数字为中文格式**
  function formatNumberToChinese(numStr) {
    numStr = numStr.replace(/,/g, "");
    const hasPlus = numStr.includes("+");
    const num = parseFloat(numStr.replace("+", ""));

    let result;
    if (num >= 100000000) {
      result = Math.round(num / 100000000) + "亿"; // 四舍五入到整数
    } else if (num >= 10000) {
      result = Math.round(num / 10000) + "万"; // 四舍五入到整数
    } else {
      result = num.toString();
    }
    if (hasPlus) {
      result += "+";
    }
    return result;
  }

  // **监听页面可见性变化**
  function visibilityChangeHandler() {
    if (document.visibilityState === "hidden") {
      stopListener(); // 切到后台时停止监听
      requestAnimationFrame(startListener); // 下次回到前台时重新监听
    }
  }

  // **开始监听**
  function startListener() {
    // 重新读取 localStorage 中的 candidates
    const updatedCandidates =
      JSON.parse(localStorage.getItem("candidates")) || [];
    candidates.length = 0; // 清空当前数组
    candidates.push(...updatedCandidates); // 更新为最新的候选清单
    renderCandidatesPanel(); // 更新候选清单面板
    document.addEventListener("visibilitychange", visibilityChangeHandler);
  }

  // **停止监听**
  function stopListener() {
    document.removeEventListener("visibilitychange", visibilityChangeHandler);
  }

  // **创建上方按钮: 加入候选**
  function createButton(text, onClickHandler) {
    const button = document.createElement("button");
    button.innerText = text;
    button.style.position = "fixed";
    button.style.top = "60px";
    button.style.right = "20px";
    button.style.padding = "10px";
    button.style.background = "#007bff";
    button.style.color = "white";
    button.style.border = "none";
    button.style.borderRadius = "5px";
    button.style.cursor = "pointer";
    button.style.zIndex = "1000";

    button.onclick = onClickHandler;
    document.body.appendChild(button);
  }

  // **创建按钮: 导出候选清单**
  function createExportButton() {
    const exportButton = document.createElement("button");
    exportButton.textContent = "导出候选清单CSV";
    exportButton.style.position = "absolute";
    exportButton.style.top = "10px";
    exportButton.style.right = "10px";
    exportButton.style.padding = "5px";
    exportButton.style.background = "#28a745";
    exportButton.style.color = "white";
    exportButton.style.border = "none";
    exportButton.style.borderRadius = "5px";
    exportButton.style.cursor = "pointer";
    exportButton.style.zIndex = "1000";

    exportButton.onclick = exportCandidateList;
    return exportButton;
  }

  // **创建候选清单面板**
  function createCandidatesPanel() {
    const panel = document.createElement("div");
    panel.id = "candidatesPanel";
    panel.style.position = "fixed";
    panel.style.bottom = "20px";
    panel.style.right = "20px";
    panel.style.width = "400px";
    panel.style.height = "200px";
    panel.style.overflowY = "auto";
    panel.style.backgroundColor = "white";
    panel.style.border = "1px solid #ccc";
    panel.style.padding = "10px";
    panel.style.zIndex = "1000";
    panel.style.display = "none"; // 默认隐藏

    const title = document.createElement("h3");
    title.innerText = "候选清单";
    panel.appendChild(title);

    // 添加导出候选清单按钮
    const exportButton = createExportButton();
    panel.appendChild(exportButton);

    // 添加清空候选清单按钮
    const clearButton = document.createElement("button");
    clearButton.innerText = "清空";
    clearButton.style.marginBottom = "10px";
    clearButton.onclick = clearCandidates;
    panel.appendChild(clearButton);

    document.body.appendChild(panel);
  }

  // **渲染候选清单**
  function renderCandidatesPanel() {
    const panel = document.getElementById("candidatesPanel");
    panel.style.display = "block"; // 显示候选面板

    // 清空现有清单
    panel.innerHTML = "<h3>候选清单</h3>";

    // 添加导出候选清单按钮
    const exportButton = createExportButton();
    panel.appendChild(exportButton);

    const clearButton = document.createElement("button");
    clearButton.innerText = "清空";
    clearButton.style.marginBottom = "10px";
    clearButton.onclick = clearCandidates;
    panel.appendChild(clearButton);

    // 渲染所有候选项
    candidates.forEach((app, index) => {
      const appDiv = document.createElement("div");
      appDiv.style.display = "flex";
      appDiv.style.justifyContent = "space-between";
      appDiv.style.marginBottom = "5px";

      const appInfo = document.createElement("span");
      appInfo.innerText = `${app.name} (${app.category})`;

      const deleteButton = document.createElement("button");
      deleteButton.innerText = "删除";
      deleteButton.style.backgroundColor = "#ff6666";
      deleteButton.style.color = "white";
      deleteButton.style.border = "none";
      deleteButton.style.borderRadius = "3px";
      deleteButton.style.cursor = "pointer";
      deleteButton.onclick = () => deleteCandidate(index);

      appDiv.appendChild(appInfo);
      appDiv.appendChild(deleteButton);
      panel.appendChild(appDiv);
    });
  }

  // **删除候选清单中的某个应用**
  function deleteCandidate(index) {
    candidates.splice(index, 1);
    localStorage.setItem("candidates", JSON.stringify(candidates));
    renderCandidatesPanel(); // 更新面板
  }

  // **清空候选清单**
  function clearCandidates() {
    candidates.length = 0;
    localStorage.setItem("candidates", JSON.stringify(candidates));
    renderCandidatesPanel(); // 更新面板
  }

  // **获取应用类别和下载量**
  function getCategoryAndDownloads() {
    // 先获取正确的父容器
    const parentContainer = document.querySelector("div.app-info-card.dd-flex");
    if (!parentContainer) {
      console.log("未找到正确的父容器!");
      return "未知类别";
    }
    // 获取该容器下所有 app-info-card-item
    const items = parentContainer.querySelectorAll("div.app-info-card-item");

    // 获取items中的第3个元素
    const blockCategory = items[2].querySelector("div.app-desc-value");
    const categoryElement = blockCategory.querySelector("a.dd-desc-color");

    // 获取items中的倒数第2个元素
    const blockDownloads =
      items[items.length - 2].querySelector("div.app-value");
    const downloadsElement = blockDownloads.querySelector("a.app-value");
    // 返回类别和下载量
    return {
      appCategory: categoryElement
        ? categoryElement.innerText.trim()
        : "未知类别",
      appDownloads: downloadsElement
        ? downloadsElement.innerText.trim()
        : "未知下载量",
    };
  }

  // **获取应用发布时间**
  function getPubTime() {
    const parentContainer = document.querySelector("div.app-base-info-wrap");
    if (!parentContainer) {
      return "-";
    }
    const item = parentContainer.querySelectorAll(
      "div.content-title.dd-flex.dd-flex-end"
    )[1];
    return item ? item.innerText.trim() : "-";
  }

  // **模拟点击"排行榜全部"按钮**
  function clickBtn() {
    const btn = document
      .querySelector("ul.filter-list.filter-group.dd-overflow-visible")
      .querySelectorAll("a.toggle-item")[0];
    if (btn) {
      btn.click();
    }
  }

  // **将存储的jsonData进行处理**
  // "rank_type": 2, 游戏榜
  // "genre_id": 33, 游戏总榜
  // "brand_id": 1, 免费
  // "brand_id": 2, 畅销
  // "brand_id": 3, 付费
  // "brand_id": 5, 人气蹿升
  function processJsonData(rankData) {
    let rank1 = "-";
    let rank2 = "-";
    let rank3 = "-";

    rankData.forEach((item) => {
      if (
        item.rank_type === 2 &&
        item.genre_id === 33 &&
        (item.brand_id === 1 || item.brand_id === 3)
      ) {
        // 免费/付费榜排名
        rank1 = item.stats.at(-1).at(-1) ? item.stats.at(-1).at(-1) : "-";
      }
      if (item.rank_type === 2 && item.genre_id === 33 && item.brand_id === 2) {
        // 畅销榜排名
        rank2 = item.stats.at(-1).at(-1) ? item.stats.at(-1).at(-1) : "-";
      }
      if (item.rank_type === 2 && item.genre_id === 33 && item.brand_id === 5) {
        // 人气蹿升榜排名
        rank3 = item.stats.at(-1).at(-1) ? item.stats.at(-1).at(-1) : "-";
      }
    });
    return { rank1, rank2, rank3 };
  }

  // **通用 CSV 导出函数**
  function exportToCSV(data, filename) {
    // 去重:基于应用名称进行去重
    const uniqueData = Array.from(new Set(data.map((app) => app.name))).map(
      (name) => {
        return data.find((app) => app.name === name);
      }
    );

    let csvContent =
      "名称,类别,免费/付费榜,畅销榜,人气蹿升榜,下载量,发布时间,链接\n";
    uniqueData.forEach((app) => {
      csvContent += `"${app.name}","${app.category}","${app.rank1}","${app.rank2}","${app.rank3}","${app.downloads}","${app.pubtime}","${app.link}"\n`;
    });

    // 生成带日期的文件名
    const fullFilename = `${filename}_${getCurrentDate()}.csv`;

    // 创建 CSV 下载链接
    const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
    const link = document.createElement("a");
    link.href = URL.createObjectURL(blob);
    link.setAttribute("download", fullFilename);
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
  }

  // **导出候选清单**
  function exportCandidateList() {
    const candidateApps = [...candidates];
    if (candidateApps.length === 0) {
      alert("候选清单为空!");
      return;
    }
    exportToCSV(candidateApps, "详情页_候选清单");
    // **清空候选清单**
    clearCandidates();
  }

  // **主逻辑**
  requestAnimationFrame(startListener); // 开始监听页面可见性变化

  createCandidatesPanel(); // 创建候选清单面板
  renderCandidatesPanel(); // 渲染候选清单
})();