Linux.do 抽奖器

在Linux.do平台上进行抽奖,支持文章切换时自动更新,以表格形式展示结果,包含用户头像和参与时间,支持时间范围选择

目前为 2024-10-07 提交的版本。查看 最新版本

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址