// ==UserScript==
// @name Linux.do 抽奖器
// @namespace http://linux.do/
// @version 1.0.1
// @description 在Linux.do平台上进行抽奖,支持文章切换时自动更新,以表格形式展示结果,包含用户头像和参与时间,支持时间范围选择
// @author PastKing
// @match https://www.linux.do/t/topic/*
// @match https://linux.do/t/topic/*
// @grant none
// @license MIT
// @icon https://cdn.linux.do/uploads/default/optimized/1X/3a18b4b0da3e8cf96f7eea15241c3d251f28a39b_2_32x32.png
// ==/UserScript==
(function() {
'use strict';
let uiElements = null;
// 创建UI元素
function createUI() {
const container = document.createElement('div');
container.style.cssText = `
background-color: #ffffff;
padding: 30px;
border-radius: 10px;
margin: 30px auto;
text-align: center;
max-width: 800px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
font-family: Arial, sans-serif;
margin-bottom: 0 !important;
`;
const title = document.createElement('h2');
title.textContent = '🎉 Linux.do 抽奖器 - @PastKing';
title.style.cssText = `
color: #2c3e50;
margin-bottom: 25px;
font-weight: bold;
`;
const dateContainer = document.createElement('div');
dateContainer.style.cssText = `
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 25px;
`;
const startDateTimeInput = document.createElement('input');
startDateTimeInput.type = 'datetime-local';
startDateTimeInput.style.cssText = `
padding: 10px;
margin: 0 10px;
border: 1px solid #bdc3c7;
border-radius: 5px;
font-size: 14px;
`;
const endDateTimeInput = document.createElement('input');
endDateTimeInput.type = 'datetime-local';
endDateTimeInput.style.cssText = startDateTimeInput.style.cssText;
dateContainer.appendChild(createLabel('开始时间:'));
dateContainer.appendChild(startDateTimeInput);
dateContainer.appendChild(createLabel('结束时间:'));
dateContainer.appendChild(endDateTimeInput);
const inputContainer = document.createElement('div');
inputContainer.style.cssText = `
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 25px;
`;
const input = document.createElement('input');
input.type = 'number';
input.min = '1';
input.placeholder = '抽取数量';
input.style.cssText = `
padding: 10px;
margin-right: 15px;
border: 1px solid #bdc3c7;
border-radius: 5px;
font-size: 14px;
width: 120px;
marginBottom: '0 !important'
`;
const button = document.createElement('button');
button.textContent = '开始抽奖';
button.style.cssText = `
padding: 10px 20px;
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
`;
button.onmouseover = () => button.style.backgroundColor = '#2980b9';
button.onmouseout = () => button.style.backgroundColor = '#3498db';
inputContainer.appendChild(input);
inputContainer.appendChild(button);
const result = document.createElement('div');
container.appendChild(title);
container.appendChild(dateContainer);
container.appendChild(inputContainer);
container.appendChild(result);
return { container, input, button, result, startDateTimeInput, endDateTimeInput };
}
function createLabel(text) {
const label = document.createElement('label');
label.textContent = text;
label.style.cssText = `
font-size: 14px;
color: #34495e;
margin-right: 5px;
`;
return label;
}
// 格式化日期
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
// 获取候选人列表
async function getCandidateList(startDateTime, endDateTime) {
const topicId = window.location.pathname.split('/')[3];
let candidateList = [];
let nameList = new Set();
const start = startDateTime ? new Date(startDateTime) : null;
const end = endDateTime ? new Date(endDateTime) : null;
for (let i = 1; i < 1000; i++) {
const response = await fetch(`/t/${topicId}.json?page=${i}`);
if (response.status === 404) break;
const result = await response.json();
const posts = result.post_stream.posts;
const topicOwner = result.details.created_by.username;
for (let post of posts) {
const postDate = new Date(post.created_at);
if ((start && postDate < start) || (end && postDate > end)) continue;
const onlyName = post.username;
if (!nameList.has(onlyName) && onlyName !== topicOwner) {
const candidate = {
only_name: onlyName,
display_name: post.display_username,
post_number: post.post_number,
created_at: post.created_at,
avatar: post.avatar_template.replace('{size}', '90')
};
candidateList.push(candidate);
nameList.add(onlyName);
}
}
}
return candidateList;
}
// 执行抽奖
async function performLottery(count, startDateTime, endDateTime) {
const candidates = await getCandidateList(startDateTime, endDateTime);
if (candidates.length === 0) {
return { error: '在选定的时间范围内没有找到任何候选人。' };
}
if (count > candidates.length) {
return { error: `抽奖人数不能多于唯一发帖人数。当前只有 ${candidates.length} 个符合条件的候选人。` };
}
const chosenPosts = [];
const winners = new Set();
while (winners.size < count && candidates.length > 0) {
const randomIndex = Math.floor(Math.random() * candidates.length);
const winner = candidates.splice(randomIndex, 1)[0];
if (!winners.has(winner.only_name)) {
winners.add(winner.only_name);
chosenPosts.push(winner);
}
}
return { winners: chosenPosts };
}
// 显示抽奖结果
function displayResults(results) {
uiElements.result.innerHTML = '<h3 style="color: #2c3e50; margin-bottom: 20px;">🏆 抽奖结果</h3>';
const table = document.createElement('table');
table.style.cssText = `
width: 100%;
border-collapse: separate;
border-spacing: 0;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
`;
const headerRow = table.insertRow();
['序号', '头像', '用户名', '楼层', '参与时间'].forEach(text => {
const th = document.createElement('th');
th.textContent = text;
th.style.cssText = `
padding: 15px;
background-color: #f2f2f2;
color: #333;
font-weight: bold;
text-align: left;
border-bottom: 2px solid #ddd;
`;
headerRow.appendChild(th);
});
results.forEach((result, index) => {
const row = table.insertRow();
row.style.backgroundColor = index % 2 === 0 ? '#ffffff' : '#f9f9f9';
const cellIndex = row.insertCell();
cellIndex.textContent = index + 1;
cellIndex.style.cssText = `
padding: 12px 15px;
text-align: center;
font-weight: bold;
color: #3498db;
`;
const cellAvatar = row.insertCell();
const avatar = document.createElement('img');
avatar.src = result.avatar.startsWith('http') ? result.avatar : `https://linux.do${result.avatar}`;
avatar.style.cssText = `
width: 40px;
height: 40px;
border-radius: 50%;
display: block;
margin: 0 auto;
border: 2px solid #3498db;
`;
cellAvatar.appendChild(avatar);
cellAvatar.style.padding = '12px 15px';
const cellUsername = row.insertCell();
const userLink = document.createElement('a');
userLink.href = `https://linux.do/u/${encodeURIComponent(result.only_name)}/summary`;
userLink.textContent = `@${result.only_name}`;
userLink.target = '_blank';
userLink.style.cssText = `
text-decoration: none;
color: #3498db;
font-weight: bold;
transition: color 0.3s;
`;
userLink.onmouseover = () => userLink.style.color = '#2980b9';
userLink.onmouseout = () => userLink.style.color = '#3498db';
cellUsername.appendChild(userLink);
cellUsername.style.cssText = `
padding: 12px 15px;
text-align: left;
`;
const cellNumber = row.insertCell();
cellNumber.textContent = `#${result.post_number}`;
cellNumber.style.cssText = `
padding: 12px 15px;
text-align: center;
color: #7f8c8d;
`;
const cellTime = row.insertCell();
cellTime.textContent = formatDate(result.created_at);
cellTime.style.cssText = `
padding: 12px 15px;
text-align: center;
color: #7f8c8d;
`;
});
uiElements.result.appendChild(table);
}
// 主函数
function main() {
uiElements = createUI();
// 插入UI到指定位置
const targetElement = document.querySelector('#post_1 > div.row');
if (targetElement) {
targetElement.parentNode.insertBefore(uiElements.container, targetElement.nextSibling);
// 强制移除目标元素的 marginBottom
function removeMarginBottom() {
targetElement.style.setProperty('margin-bottom', '0', 'important');
const computedStyle = window.getComputedStyle(targetElement);
if (computedStyle.getPropertyValue('margin-bottom') !== '0px') {
targetElement.style.setProperty('margin-bottom', '-9px', 'important');
}
}
removeMarginBottom();
const observer = new MutationObserver(removeMarginBottom);
observer.observe(targetElement, { attributes: true, attributeFilter: ['style'] });
setInterval(removeMarginBottom, 100);
} else {
console.error('无法找到目标插入位置');
return;
}
uiElements.button.addEventListener('click', async () => {
const count = parseInt(uiElements.input.value);
if (isNaN(count) || count < 1) {
uiElements.result.innerHTML = '<p style="color: #e74c3c; font-weight: bold;">请输入有效的抽取数量。</p>';
return;
}
const startDateTime = uiElements.startDateTimeInput.value ? new Date(uiElements.startDateTimeInput.value) : null;
const endDateTime = uiElements.endDateTimeInput.value ? new Date(uiElements.endDateTimeInput.value) : null;
if (startDateTime && endDateTime && startDateTime > endDateTime) {
uiElements.result.innerHTML = '<p style="color: #e74c3c; font-weight: bold;">开始时间不能晚于结束时间。</p>';
return;
}
uiElements.button.disabled = true;
uiElements.button.textContent = '抽奖中...';
uiElements.button.style.backgroundColor = '#bdc3c7';
uiElements.result.innerHTML = '<p style="color: #3498db; font-weight: bold;">正在抽奖,请稍候...</p>';
const lotteryResults = await performLottery(count, startDateTime, endDateTime);
if (lotteryResults.error) {
uiElements.result.innerHTML = `<p style="color: #e74c3c; font-weight: bold;">${lotteryResults.error}</p>`;
} else {
displayResults(lotteryResults.winners);
}
uiElements.button.disabled = false;
uiElements.button.textContent = '开始抽奖';
uiElements.button.style.backgroundColor = '#3498db';
});
}
// 运行主函数
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', main);
} else {
main();
}
})();