Greasy Fork is available in English.

中文PT站官邀查询器

中国私人追踪器官方招募路线

// ==UserScript==
// @name           中文PT站官邀查询器
// @namespace      https://greasyfork.org/zh-CN/users/1270887-co-ob
// @version        0.6
// @description    中国私人追踪器官方招募路线
// @author         ChatGPT, cO_ob
// @match          *://tieba.baidu.com/*
// @grant          none
// @license        MIT
// ==/UserScript==

(function() {
  'use strict';
  const dataUrl = 'https://or.jpt.com.np/data.json';
  const inviteRouteButton = document.createElement('button');
  inviteRouteButton.innerText = '💊';
  inviteRouteButton.id = 'inviteRouteButton';
  document.body.appendChild(inviteRouteButton);

  inviteRouteButton.addEventListener('click', (event) => {
    const form = document.getElementById('routeCalcForm');
    const overlay = document.getElementById('overlay');
    const isFormVisible = form.style.display === 'block';

    form.style.display = isFormVisible ? 'none' : 'block';
    overlay.style.display = isFormVisible ? 'none' : 'block';
    event.stopPropagation();
  });

  async function fetchData() {
    try {
      const response = await fetch(dataUrl);
      if (!response.ok) {
        throw new Error('Network response was not ok');
      }
      return await response.json();
    } catch (error) {
      console.error('Error fetching data:', error);
      return null;
    }
  }

  window.addEventListener('load', async function() {
    const data = await fetchData();
    if (!data) return;
    const { routeInfo, unlockInviteClass, classInfo, nicknameList } = data;
    var style = document.createElement('style');
    style.textContent = `
      :root {
    --light: hsl(0, 0%, 100%);
    --background: linear-gradient(to right bottom, hsl(236, 50%, 50%), hsl(195, 50%, 50%));
      }

      #overlay {
        display: none;
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background-color: rgba(0, 0, 0, 0.5);
        z-index: 9000;
      }

      #inviteRouteButton {
        display: flex;
        position: fixed;
        bottom: 20px;
        right: 20px;
        width: 60px;
        height: 60px;
        align-items: center;
        justify-content: center;
        border-radius: 50%;
        background-color: rgba(128, 128, 128, 0.5);
        color: white;
        border: none;
        font-size: 24px;
        cursor: pointer;
      }

      #routeCalcForm {
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        width: 700px;
        height: 400px;
        padding: 20px;
        background: var(--background);
        border-radius: 12px;
        z-index: 9001;
        user-select: none;
      }

      #routeCalcForm button {
        margin-bottom: 10px;
        padding: 16px 16px;
        font-family: inherit;
        font-size: 16px;
        color: var(--light);
        background: transparent;
        letter-spacing: 2px;
        cursor: pointer;
      }

      #headerTable {
        margin: 0 auto;
        width: 100vw;
        height: 80px;
        max-height: 80px;
        table-layout: fixed;
        text-align: center;
        max-width: calc(min(100%,1080px));
        border-collapse: collapse;
        overflow: hidden;
      }

      .col1, .col5 {
        width: 10%;
      }

      .col2, .col4 {
        width: 20%;
        text-align: center;
        position: relative;
      }

      .col3 {
        width: 40%;
      }

      td {
        overflow: hidden;
      }

      #routeChain {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 70%;
        max-height: 78px;
        font-size: 16px;
        color: var(--light);
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: normal;
      }

      #target, #source {
        display: block;
        position: absolute;
        left: 10px;
        right: 10px;
        top: -5px;
        font-size: 16px;
        font-family: inherit;
        color: var(--light);
        background: transparent;
        cursor: pointer;
        z-index: 2;
        padding-top: 60px;
      }

      #sourceNickname, #targetNickname {
        display: inline-block;
        position: relative;
        width: 100%;
        top: -15%;
        font-weight: bold;
        font-size: 32px;
        color: var(--light);
        z-index: 1;
      }

      #prevroute, #nextroute {
        display: none;
        position: relative;
        top:8px;
        font-size: calc(max(2.5vw, 32px));
        color: var(--light);
        letter-spacing: 1px;
        cursor: pointer;
      }

      #chainInfo {
        color: var(--light);
        height: 30%;
        font-size: 16px;
        text-align: center;
        border-collapse: collapse;
      }

      #detailsTable {
        padding: 10px;
        color: var(--light);
        font-size: 13px;
        max-height: 300px;
        overflow: auto;
        line-height: 1.6;
      }

      #detailsTable, #gridContainer {
        -ms-overflow-style: none;
        scrollbar-width: none;
      }

      #detailsTable::-webkit-scrollbar,
      #gridContainer::-webkit-scrollbar {
        display: none;
      }

      .separator {
        height: 1px;
        background-color: hsla(0, 0%, 100%, .4);
        margin: 10px 0;
      }

      #gridContainer {
        display: none;
        position: absolute;
        top: 130px;
        left: 60px;
        width: 600px;
        max-height: 240px;
        padding: 10px;
        background: #f9f9f9;
        box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
        border-radius: 8px;
        overflow: hidden;
        overflow-y: auto;
        z-index: 9002;
      }

      #gridContainer div {
        padding: 8px;
        background: #fff;
        font-size: 13px;
        text-align: center;
        cursor: pointer;
        white-space: nowrap;
        min-width: max-content;
      }
    `;

    document.head.appendChild(style);

    const overlay = document.createElement('div');
    overlay.id = 'overlay';
    document.body.appendChild(overlay);

    const inviteForm = document.createElement('form');
    inviteForm.id = 'routeCalcForm';
    inviteForm.style.display = 'none';
    inviteForm.innerHTML = `
      <table id="headerTable">
          <tr>
              <td class="col1"><div id="prevroute">&#8249;&#8249;</div></td>
              <td class="col2">
                <div id="sourceNickname">始发站</div>
                <div id="source">From</div>
              </td>
              <td class="col3">            <table>
              <tr>
                <div id="routeChain"></div>
              </tr>
              <tr>
                <div id="chainInfo"></div>
              </tr>
            </table></td>
              <td class="col4">
                <div id="targetNickname">终点站</div>
                <div id="target">To</div>

              </td>
              <td class="col5"><div id="nextroute">&#8250;&#8250;</div></td>
          </tr>
      </table>
      <div id="detailsTable"></div>
      <div id="gridContainer"></div>

    `;

    document.body.appendChild(inviteForm);

    document.addEventListener('click', function(event) {
      var routeCalcForm = document.getElementById('routeCalcForm');
      var overlay = document.getElementById('overlay');
      if (!routeCalcForm.contains(event.target) && event.target !== inviteRouteButton) {
        routeCalcForm.style.display = 'none';
        overlay.style.display = 'none';
      }
    });

    document.getElementById('source').addEventListener('click', function() {
          const gridContainer = document.getElementById('gridContainer');
          if (gridContainer.style.display === 'none' || gridContainer.style.display === '') {
            populateGrid('source');
            gridContainer.style.display = 'block';
          } else {
            gridContainer.style.display = 'none';
          }
    });

    document.getElementById('target').addEventListener('click', function() {
      const gridContainer = document.getElementById('gridContainer');
      if (gridContainer.style.display === 'none' || gridContainer.style.display === '') {
        populateGrid('target');
        gridContainer.style.display = 'block';
      } else {
        gridContainer.style.display = 'none';
      }
    });

    document.getElementById('prevroute').addEventListener('click', function() {
      showRoute(-1);
    });

    document.getElementById('nextroute').addEventListener('click', function() {
      showRoute(1);
    });

    document.addEventListener('click', function(event) {
      const gridContainer = document.getElementById('gridContainer');
      if (gridContainer.style.display === 'block' && !gridContainer.contains(event.target) && event.target.id !== 'source' && event.target.id !== 'target') {
        gridContainer.style.display = 'none';
      }
    });

    let allRoutes = [];
    let currentRouteIndex = 0;

    function populateGrid(buttonId) {
      const gridContainer = document.getElementById('gridContainer');
      gridContainer.innerHTML = '';

      const uniqueKeys = Array.from(new Set(
        Object.keys(routeInfo)
        .flatMap(startKey => [startKey, ...Object.keys(routeInfo[startKey])])
      ))
      .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));

      if (buttonId === 'source') {
        uniqueKeys.unshift('From');
      } else if (buttonId === 'target') {
        uniqueKeys.unshift('To');
      }

      const container = document.createElement('div');
      container.style.display = 'grid';
      container.style.gridTemplateColumns = 'repeat(auto-fit, minmax(100px, auto))';
      container.style.gap = '0px';

      uniqueKeys.forEach(key => {
        const item = document.createElement('div');
        item.textContent = key;
        document.getElementById('gridContainer').appendChild(item);
        item.addEventListener('click', () => {
          if (buttonId === 'source') {
            if (key === 'From') {
              setTextContent('source', 'From');
              setTextContent('sourceNickname', '始发站');
            } else {
              setTextContent('source', key);
              setTextContent('sourceNickname', nicknameList[key] || key);
            }
          } else if (buttonId === 'target') {
            if (key === 'To') {
              setTextContent('target', 'To');
              setTextContent('targetNickname', '终点站');
            } else {
              setTextContent('target', key);
              setTextContent('targetNickname', nicknameList[key] || key);
            }
          }
          gridContainer.style.display = 'none';
          calculateRoute();
        });
        container.appendChild(item);
      });

      gridContainer.appendChild(container);
    }

    function calculateRoute() {
      const resultTitle = document.getElementById('routeChain');
      const resultDescription = document.getElementById('detailsTable');
      const chainInfo = document.getElementById('chainInfo');
      const prevRouteButton = document.getElementById('prevroute');
      const nextRouteButton = document.getElementById('nextroute');

      resultTitle.innerText = '';
      chainInfo.innerText = '';
      resultDescription.innerHTML = '';
      prevRouteButton.style.display = 'none';
      nextRouteButton.style.display = 'none';

      const start = document.getElementById('source').textContent.trim();
      const end = document.getElementById('target').textContent.trim();

      if (start === end) {
        resultTitle.innerText = '注册多个账号的用户将被禁止!';
        resultDescription.innerText = '';
        return;
      }

      if (start === 'From') {
        if (end === 'To') {
          resultTitle.innerText = '选一个';
          resultDescription.innerText = '';
          return;
        }

        allRoutes = Object.keys(routeInfo)
          .filter(node => routeInfo[node] && routeInfo[node][end])
          .map(node => [node, end]);

        allRoutes = sortRoutesByTime(allRoutes);
        showAllRoutes();
      } else if (end === 'To') {
        if (!routeInfo[start]) {
          resultTitle.innerText = '没收录';
          resultDescription.innerText = '';
          return;
        }
        allRoutes = Object.keys(routeInfo[start])
          .map(node => [start, node])
          .filter(([from, to]) => routeInfo[from] && routeInfo[from][to])
          .map(([from, to]) => [from, to]);

        allRoutes = sortRoutesByTime(allRoutes);
        showAllRoutes();
      } else {
        if (!routeInfo[start]) {
          resultTitle.innerText = '没收录';
          resultDescription.innerText = '';
          return;
        }
        allRoutes = findAllRoutes(start, end);
        allRoutes = sortRoutesByTime(allRoutes);
        showRoute(0);
      }
    }

    function showAllRoutes() {
      const resultTitle = document.getElementById('routeChain');
      const resultDescription = document.getElementById('detailsTable');
      const chainInfo = document.getElementById('chainInfo');

      resultTitle.innerText = '';
      resultDescription.innerHTML = '';
      chainInfo.innerText = '';

      allRoutes.forEach(route => {
        const details = route.map((node, index) => {
          if (index < route.length - 1 && routeInfo[node] && routeInfo[node][route[index + 1]]) {
            const nextNode = route[index + 1];
            const maxDays = getMaxDays(node, nextNode);
            const [time, userclass, requirement, lastActivity] = routeInfo[node][nextNode];
            const formattedDate = formatDate(lastActivity);

            let baseText = `<div class="separator"></div>${node} -> ${nextNode} 时间:${maxDays}天 ${userclass ? `申请等级:${userclass}` : ''} 最近活动:${formattedDate}`;

            if (requirement) {
              baseText += `<br>额外要求:${requirement}`;
            }

            return baseText;
          }
          return '';
        });

        const totalTime = route.reduce((sum, node, index) => {
          if (index < route.length - 1 && routeInfo[node] && routeInfo[node][route[index + 1]]) {
            return sum + getMaxDays(node, route[index + 1]);
          }
          return sum;
        }, 0);

        chainInfo.innerText = `共找到 ${allRoutes.length} 条直达路线`;
        resultDescription.innerHTML += details.join('');
      });
    }

    function showRoute(direction) {
      if (allRoutes.length === 0) return;

      currentRouteIndex = (currentRouteIndex + direction + allRoutes.length) % allRoutes.length;

      const route = allRoutes[currentRouteIndex];
      const resultTitle = document.getElementById('routeChain');
      const resultDescription = document.getElementById('detailsTable');
      const prevRouteButton = document.getElementById('prevroute');
      const nextRouteButton = document.getElementById('nextroute');
      const chainInfo = document.getElementById('chainInfo');
      const totalRoutes = allRoutes.length;
      const routeCountText = `第 ${currentRouteIndex + 1} / ${totalRoutes} 条`;

        function getAbbreviation(name) {
          return nicknameList[name] || name;
        }

        const abbreviatedRoute = route.map(node => getAbbreviation(node));

        let displayRoute;
        if (abbreviatedRoute.length === 2) {
          displayRoute = ' -> ';
        } else if (abbreviatedRoute.length > 2) {
          const middleItems = abbreviatedRoute.slice(1, -1);
          displayRoute = middleItems.length > 0 ? ` -> ${middleItems.join(' -> ')} -> ` : '';
        } else {
          displayRoute = abbreviatedRoute[0] || '';
        }

      resultTitle.innerText = displayRoute;

      const details = route.map((node, index) => {
        console.log("Processing node:", node);
        if (index < route.length - 1 && routeInfo[node] && routeInfo[node][route[index + 1]]) {
          const nextNode = route[index + 1];
          const maxDays = getMaxDays(node, nextNode);
          const [time, userclass, requirement, lastActivity] = routeInfo[node][nextNode];
          const formattedDate = formatDate(lastActivity);

          const fromUserClassIndex = Object.keys(classInfo[node]).indexOf(userclass);
          const unlockUserClassIndex = Object.keys(classInfo[node]).indexOf(unlockInviteClass[node]);

          const higherUserClass = (unlockUserClassIndex !== -1)
          ? (fromUserClassIndex > unlockUserClassIndex ? userclass : unlockInviteClass[node])
          : userclass;

          let upgradeReqText = '';
          if (higherUserClass && Object.keys(classInfo[node]).includes(higherUserClass)) {
            const upgradeReq = classInfo[node][higherUserClass];
            const upgradeReqParts = [];
            if (upgradeReq[0]) upgradeReqParts.push(`注册时间${upgradeReq[0]}天`);
            if (upgradeReq[1]) upgradeReqParts.push(`下载量${upgradeReq[1]}`);
            if (upgradeReq[2]) upgradeReqParts.push(`分享率${upgradeReq[2]}`);
            if (upgradeReq[3]) upgradeReqParts.push(upgradeReq[3]);

            upgradeReqText = upgradeReqParts.join(' & ');
          }

          let baseText = `<div class="separator"></div>${node} -> ${nextNode} 时间:${maxDays}天 ${unlockInviteClass[node] ? ` 解锁等级:${unlockInviteClass[node]}` : ''} ${userclass ? `申请等级:${userclass}` : ''} 最近活动:${formattedDate}`;

          if (userclass || unlockInviteClass[node]) {
            baseText += `<br>升级要求:${upgradeReqText}`;
          }

          if (requirement) {
            baseText += `<br>额外要求:${requirement}`;
          }

          return baseText;
        }
        return '';
      });
      chainInfo.innerText = `${routeCountText} 换乘 ${route.length - 2} 次 耗时 ${route.reduce((sum, node, index) => {
        if (index < route.length - 1 && routeInfo[node] && routeInfo[node][route[index + 1]]) {
          return sum + getMaxDays(node, route[index + 1]);
        }
        return sum;
      }, 0)} 天`;
      resultDescription.innerHTML = details.join('');

      prevRouteButton.style.display = currentRouteIndex === 0 ? 'none' : 'inline';
      nextRouteButton.style.display = currentRouteIndex === allRoutes.length - 1 ? 'none' : 'inline';
    }

    function sortRoutesByTime(routes) {
      return routes.sort((a, b) => {
        const timeA = a.reduce((sum, node, index) => {
          if (index < a.length - 1 && routeInfo[node] && routeInfo[node][a[index + 1]]) {
            return sum + getMaxDays(node, a[index + 1]);
          }
          return sum;
        }, 0);

        const timeB = b.reduce((sum, node, index) => {
          if (index < b.length - 1 && routeInfo[node] && routeInfo[node][b[index + 1]]) {
            return sum + getMaxDays(node, b[index + 1]);
          }
          return sum;
        }, 0);

        return timeA - timeB;
      });
    }

    function getMaxDays(start, end) {
      const days1 = routeInfo[start][end][0];
      const userclassRequirement = routeInfo[start][end][1];
      const days2 = (userclassRequirement === '') ? 0 : classInfo[start][userclassRequirement][0];
      let days3 = 0;
      const unlockUserClass = unlockInviteClass[start];
      if (unlockUserClass) {
        days3 = classInfo[start][unlockUserClass][0];
      }
      return Math.max(days1, days2, days3);
    }

    function findAllRoutes(start, end) {
      const result = [];
      const stack = [[start, [start]]];

      while (stack.length) {
        const [node, route] = stack.pop();

        if (node === end) {
          result.push(route);
          continue;
        }

        for (const [next, _] of Object.entries(routeInfo[node] || {})) {
          if (!route.includes(next)) {
            stack.push([next, [...route, next]]);
          }
        }
      }

      return result;
    }

    function formatDate(dateString) {
      const year = Math.floor(dateString / 100);
      const month = dateString % 100;
      return `20${year}年${month.toString().padStart(2, '0')}月`;
    }

    function setTextContent(id, text) {
      document.getElementById(id).textContent = text;
    }

  });
})();