// ==UserScript==
// @name Nodeloc抽奖帖子排序器
// @namespace http://www.nodeloc.com/
// @version 1.0.0
// @description 为nodeloc.com论坛的抽奖帖子添加参与者排序功能,按照奖券数量排序
// @author Assistant
// @match https://www.nodeloc.com/t/topic/*
// @match https://nodeloc.cc/t/topic/*
// @license MIT
// @icon https://www.nodeloc.com/uploads/default/original/1X/8ab9e33c8eed4135d9f2b8af6e6b7cc16ec4228e.png
// @grant none
// ==/UserScript==
(function() {
'use strict';
let isSorted = false;
let originalOrder = [];
// 检查是否为抽奖帖子
function isLotteryPost() {
const lotteryContainer = document.querySelector('#post_1 > div.post__row.row > div.post__body.topic-body.clearfix > div.post__regular.regular.post__contents.contents > div > div.lottery-container');
return !!lotteryContainer;
}
// 获取参与者列表容器
function getParticipantContainer() {
return document.querySelector('#post_1 > div.post__row.row > div.post__body.topic-body.clearfix > div.post__regular.regular.post__contents.contents > div > div.lottery-container > div.lottery-participants > div');
}
// 创建查看排序按钮
function createSortButton() {
const button = document.createElement('button');
button.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="margin-right: 6px; vertical-align: middle;">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
查看排行榜
`;
button.id = 'lottery-sort-button';
button.style.cssText = `
padding: 8px 16px;
font-size: 12px;
font-weight: 600;
color: #ffffff;
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.35);
display: inline-flex;
align-items: center;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
user-select: none;
margin-left: 10px;
margin-top: 5px;
`;
// 悬停效果
button.onmouseover = function() {
this.style.background = 'linear-gradient(135deg, #2563eb 0%, #1e40af 100%)';
this.style.transform = 'scale(1.05)';
this.style.boxShadow = '0 4px 12px rgba(59, 130, 246, 0.45)';
};
button.onmouseout = function() {
this.style.background = 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)';
this.style.transform = 'scale(1)';
this.style.boxShadow = '0 2px 8px rgba(59, 130, 246, 0.35)';
};
// 点击效果
button.onmousedown = function() {
this.style.transform = 'scale(0.95)';
};
button.onmouseup = function() {
this.style.transform = 'scale(1.05)';
};
return button;
}
// 提取参与者信息
function extractParticipantInfo(participantElement) {
const titleAttr = participantElement.getAttribute('title');
if (!titleAttr) return null;
// 匹配格式:用户名(数字 奖券)
const match = titleAttr.match(/^(.+?)((\d+)\s*奖券\)$/);
if (!match) return null;
// 获取头像
const img = participantElement.querySelector('img');
const avatar = img ? img.getAttribute('src') : '';
const userUrl = participantElement.getAttribute('href') || '';
return {
username: match[1],
tickets: parseInt(match[2]),
avatar: avatar,
userUrl: userUrl,
element: participantElement
};
}
// 获取所有参与者
function getAllParticipants(container) {
const participantLinks = container.querySelectorAll('a[title*="奖券"]');
const participants = [];
participantLinks.forEach(link => {
const info = extractParticipantInfo(link);
if (info) {
participants.push(info);
}
});
return participants;
}
// 排序参与者(按奖券数量从大到小)
function sortParticipants(participants) {
return participants.sort((a, b) => b.tickets - a.tickets);
}
// 创建排行榜对话框
function createRankingDialog(participants) {
// 创建背景遮罩
const overlay = document.createElement('div');
overlay.id = 'lottery-ranking-overlay';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
`;
// 创建对话框
const dialog = document.createElement('div');
dialog.style.cssText = `
background: white;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
max-width: 600px;
max-height: 80vh;
width: 90%;
overflow: hidden;
position: relative;
`;
// 创建标题栏
const header = document.createElement('div');
header.style.cssText = `
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
color: white;
padding: 20px 24px;
font-size: 18px;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
`;
header.innerHTML = `
<span>🏆 抽奖排行榜 (共 ${participants.length} 人参与)</span>
<button id="close-ranking-dialog" style="
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background 0.2s;
" onmouseover="this.style.background='rgba(255,255,255,0.2)'" onmouseout="this.style.background='none'">×</button>
`;
// 创建内容区域
const content = document.createElement('div');
content.style.cssText = `
max-height: 500px;
overflow-y: auto;
padding: 0;
`;
// 创建表格
const table = document.createElement('table');
table.style.cssText = `
width: 100%;
border-collapse: collapse;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
`;
// 创建表头
const thead = document.createElement('thead');
thead.innerHTML = `
<tr style="background: #f8fafc; border-bottom: 2px solid #e2e8f0;">
<th style="padding: 12px 16px; text-align: left; font-weight: 600; color: #374151; width: 60px;">排名</th>
<th style="padding: 12px 16px; text-align: left; font-weight: 600; color: #374151; width: 60px;">头像</th>
<th style="padding: 12px 16px; text-align: left; font-weight: 600; color: #374151;">用户名</th>
<th style="padding: 12px 16px; text-align: center; font-weight: 600; color: #374151; width: 100px;">奖券数</th>
</tr>
`;
table.appendChild(thead);
// 创建表体
const tbody = document.createElement('tbody');
participants.forEach((participant, index) => {
const row = document.createElement('tr');
row.style.cssText = `
border-bottom: 1px solid #e2e8f0;
transition: background 0.2s;
`;
row.onmouseover = function() { this.style.background = '#f8fafc'; };
row.onmouseout = function() { this.style.background = 'white'; };
// 排名徽章样式
let rankBadge = `<span style="
background: #e2e8f0;
color: #64748b;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
">${index + 1}</span>`;
if (index === 0) {
rankBadge = `<span style="
background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
">🥇 ${index + 1}</span>`;
} else if (index === 1) {
rankBadge = `<span style="
background: linear-gradient(135deg, #9ca3af 0%, #6b7280 100%);
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
">🥈 ${index + 1}</span>`;
} else if (index === 2) {
rankBadge = `<span style="
background: linear-gradient(135deg, #cd7c0f 0%, #92400e 100%);
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
">🥉 ${index + 1}</span>`;
}
row.innerHTML = `
<td style="padding: 12px 16px; text-align: left;">${rankBadge}</td>
<td style="padding: 12px 16px; text-align: left;">
<img src="${participant.avatar}" style="
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid #e2e8f0;
" onerror="this.style.display='none'">
</td>
<td style="padding: 12px 16px; text-align: left;">
<a href="${participant.userUrl}" target="_blank" style="
color: #3b82f6;
text-decoration: none;
font-weight: 500;
" onmouseover="this.style.textDecoration='underline'" onmouseout="this.style.textDecoration='none'">
${participant.username}
</a>
</td>
<td style="padding: 12px 16px; text-align: center;">
<span style="
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
color: white;
padding: 4px 12px;
border-radius: 16px;
font-size: 12px;
font-weight: 600;
">${participant.tickets} 张</span>
</td>
`;
tbody.appendChild(row);
});
table.appendChild(tbody);
content.appendChild(table);
dialog.appendChild(header);
dialog.appendChild(content);
overlay.appendChild(dialog);
// 关闭对话框事件
const closeButton = header.querySelector('#close-ranking-dialog');
const closeDialog = () => {
overlay.style.opacity = '0';
setTimeout(() => {
if (overlay.parentNode) {
overlay.remove();
}
}, 200);
};
closeButton.addEventListener('click', closeDialog);
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
closeDialog();
}
});
// 显示动画
overlay.style.opacity = '0';
overlay.style.transition = 'opacity 0.2s ease-out';
document.body.appendChild(overlay);
setTimeout(() => {
overlay.style.opacity = '1';
}, 10);
return overlay;
}
// 显示成功提示
function showToast(message, type = 'success') {
// 移除已存在的提示
const existingToast = document.getElementById('lottery-sort-toast');
if (existingToast) {
existingToast.remove();
}
const toast = document.createElement('div');
toast.id = 'lottery-sort-toast';
const bgColor = type === 'success'
? 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)'
: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)';
const icon = type === 'success'
? '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>'
: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>';
toast.innerHTML = `
<div style="display: flex; align-items: center;">
${icon}
<span style="color: #ffffff; font-weight: 500; margin-left: 8px;">${message}</span>
</div>
`;
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${bgColor};
color: white;
padding: 12px 20px;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
font-size: 14px;
transform: translateX(100%);
transition: transform 0.3s ease-out;
`;
document.body.appendChild(toast);
// 显示动画
setTimeout(() => {
toast.style.transform = 'translateX(0)';
}, 100);
// 自动隐藏
setTimeout(() => {
toast.style.transform = 'translateX(100%)';
setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
}, 300);
}, 3000);
}
// 处理查看排行榜点击
function handleRankingClick() {
const container = getParticipantContainer();
if (!container) {
showToast('未找到参与者列表', 'error');
return;
}
// 获取参与者信息
const participants = getAllParticipants(container);
if (participants.length === 0) {
showToast('未找到参与者信息', 'error');
return;
}
// 排序
const sortedParticipants = sortParticipants(participants);
// 显示排行榜对话框
createRankingDialog(sortedParticipants);
}
// 初始化脚本
function init() {
// 等待页面加载完成
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
return;
}
// 检查是否为抽奖帖子
if (!isLotteryPost()) {
console.log('当前不是抽奖帖子');
return;
}
// 查找参与者容器
const container = getParticipantContainer();
if (!container) {
console.log('未找到参与者列表容器');
return;
}
// 检查是否已经添加了排序按钮
if (document.getElementById('lottery-sort-button')) {
return;
}
// 创建并添加查看排行榜按钮
const sortButton = createSortButton();
sortButton.addEventListener('click', handleRankingClick);
// 将按钮添加到参与者容器后面
container.parentNode.insertBefore(sortButton, container.nextSibling);
console.log('抽奖排序按钮已添加');
}
// 监听页面变化(处理SPA导航)
function observePageChanges() {
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'childList') {
// 检查是否有新的内容加载
setTimeout(init, 1000); // 延迟执行,确保内容完全加载
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// 启动脚本
init();
observePageChanges();
})();