// ==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">‹‹</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">››</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;
}
});
})();