Linux.do 抽奖器

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Linux.do 抽奖器
// @namespace    http://linux.do/
// @version      1.0.3
// @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;

    // 首先获取主题信息以确定总页数
    const initialResponse = await fetch(`/t/${topicId}.json`);
    const initialData = await initialResponse.json();
    const totalPosts = initialData.posts_count;
    const totalPages = Math.ceil(totalPosts / 20); // 每页20个帖子
    const topicOwner = initialData.details.created_by.username;

    // 更新进度显示
    const progressDiv = document.createElement("div");
    progressDiv.style.cssText = `
        margin: 10px 0;
        padding: 10px;
        background-color: #f8f9fa;
        border-radius: 5px;
    `;
    uiElements.result.appendChild(progressDiv);

    // 分批处理页面
    const batchSize = 5; // 每批处理的页面数
    for (let page = 1; page <= totalPages; page += batchSize) {
      const batchPromises = [];

      // 创建这一批次的请求
      for (let i = 0; i < batchSize && page + i <= totalPages; i++) {
        const currentPage = page + i;
        batchPromises.push(
          fetch(`/t/${topicId}.json?page=${currentPage}`).then((response) =>
            response.ok ? response.json() : null
          )
        );
      }

      // 等待这一批次的所有请求完成
      const results = await Promise.all(batchPromises);

      // 处理结果
      results.forEach((result) => {
        if (result && result.post_stream && result.post_stream.posts) {
          result.post_stream.posts.forEach((post) => {
            const postDate = new Date(post.created_at);
            if ((start && postDate < start) || (end && postDate > end)) return;

            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);
            }
          });
        }
      });

      // 更新进度
      const progress = Math.min(
        100,
        Math.round(((page + batchSize - 1) / totalPages) * 100)
      );
      progressDiv.innerHTML = `正在加载数据... ${progress}% (${candidateList.length} 个候选人)`;

      // 添加短暂延迟以避免请求过快
      await new Promise((resolve) => setTimeout(resolve, 1000));
    }

    progressDiv.remove();
    return candidateList;
  }
  // 执行抽奖
  async function performLottery(count, startDateTime, endDateTime) {
    uiElements.result.innerHTML =
      '<p style="color: #3498db; font-weight: bold;">正在收集候选人数据...</p>';

    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;">🏆 抽奖结果 <button id="copyAllButton" style="padding: 5px 10px; background-color: #e67e22; color: white; border: none; border-radius: 5px; font-size: 14px; cursor: pointer;">一键复制全体中奖信息</button></h3>';
    const copyAllButton = document.getElementById("copyAllButton");
    copyAllButton.onclick = () => {
      const winnerNames = results
        .map((result) => `@${result.only_name}`)
        .join(", ");
      const currentDate = new Date().toLocaleString("zh-CN", {
        year: "numeric",
        month: "numeric",
        day: "numeric",
        hour: "2-digit",
        minute: "2-digit",
        second: "2-digit",
        hour12: false,
      });
      const markdownText = `🎉📢 恭喜以下幸运用户成功中奖:\n   ${winnerNames}\n\n📅 开奖日期:${currentDate}\n🎁 奖品信息:\n   \n\n✨ 再次感谢所有参与者的热情支持!\n💫 未中奖的小伙伴也不要灰心,继续关注我们的后续活动哦~\n\n**请中奖用户及时关注私信**`;
      navigator.clipboard.writeText(markdownText).then(
        () => {
          alert("全体中奖信息已复制到剪贴板!");
        },
        () => {
          alert("复制失败,请手动复制。");
        }
      );
    };

    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;
            `;

      const cellCopy = row.insertCell();
      const copyButton = document.createElement("button");
      copyButton.textContent = "复制信息";
      copyButton.style.cssText = `
                padding: 5px 10px;
                background-color: #2ecc71;
                color: white;
                border: none;
                border-radius: 5px;
                font-size: 14px;
                cursor: pointer;
            `;
      copyButton.onclick = () => {
        const currentDate = new Date().toLocaleDateString("zh-CN", {
          year: "numeric",
          month: "long",
          day: "numeric",
          hour: "2-digit",
          minute: "2-digit",
          second: "2-digit",
        });
        const markdownText = `🎉🎊 热烈祝贺 @${result.only_name}!成功中奖!🏆\n\n📅 中奖日期: ${currentDate}\n🔢 幸运楼层: #${result.post_number}\n🎁 获得奖品:\n   -   (具体奖品信息请查看活动详情)\n\n🙏 感谢你的热情参与和支持!\n🌟 希望你能继续关注我们的后续活动哦~`;
        navigator.clipboard.writeText(markdownText).then(
          () => {
            alert("信息已复制到剪贴板!");
          },
          () => {
            alert("复制失败,请手动复制。");
          }
        );
      };
      cellCopy.appendChild(copyButton);
      cellCopy.style.cssText = `
                padding: 12px 15px;
                text-align: center;
            `;
    });
    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();
  }
})();