学习通自动抢讲座

简单的学习通讲座抢名额脚本:右上角 GUI,可实时开关、可调刷新间隔、日志面板。

// ==UserScript==
// @name         学习通自动抢讲座
// @version      0.7.0
// @description  简单的学习通讲座抢名额脚本:右上角 GUI,可实时开关、可调刷新间隔、日志面板。
// @license      MIT
// @author       和川
// @match        *.chaoxing.com/page/*/show
// @icon         https://statics.chaoxing.com/favicon.ico
// @grant        GM_notification
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-idle
// @namespace https://greasyfork.org/users/1471327
// ==/UserScript==

(function () {
  'use strict';

  /* ---------- 状态持久化 ---------- */
  let enabled = GM_getValue('enabled', true);          // 自动抢开关
  let logVisible = GM_getValue('logVisible', false);   // 日志面板显隐
  let refreshInterval = GM_getValue('refreshInterval', 3000); // 刷新间隔(ms)

  /* ---------- UI: 右上角控制面板 ---------- */
  const ctrlWrap = document.createElement('div');
  ctrlWrap.style.cssText = `
    position: fixed;
    top: 20px;
    right: 20px;
    z-index: 2147483647;
    display: flex;
    gap: 8px;`;

  const toggleBtn = document.createElement('button');
  Object.assign(toggleBtn.style, baseBtnStyle(enabled ? '#4caf50' : '#f44336'));
  toggleBtn.textContent = `自动抢: ${enabled ? 'ON' : 'OFF'}`;
  ctrlWrap.appendChild(toggleBtn);

  const logBtn = document.createElement('button');
  Object.assign(logBtn.style, baseBtnStyle('#2196f3'));
  logBtn.textContent = logVisible ? '隐藏日志' : '显示日志';
  ctrlWrap.appendChild(logBtn);

  const refreshInput = document.createElement('input');
  refreshInput.type = 'number';
  refreshInput.min = '500';
  refreshInput.step = '500';
  refreshInput.value = refreshInterval;
  refreshInput.title = '刷新间隔(ms)';
  refreshInput.style.cssText = `
    width: 80px;
    padding: 8px 4px;
    border: 1px solid #ccc;
    border-radius: 6px;`;
  ctrlWrap.appendChild(refreshInput);

  const refreshLabel = document.createElement('span');
  refreshLabel.textContent = ' ms';
  refreshLabel.style.color = '#fff';
  ctrlWrap.appendChild(refreshLabel);

  document.body.appendChild(ctrlWrap);

  /* ---------- 日志面板 ---------- */
  const logPanel = document.createElement('pre');
  logPanel.id = 'cxg-log-panel';
  logPanel.style.cssText = `
    position: fixed;
    top: 60px;
    right: 20px;
    width: 360px;
    height: 220px;
    background: rgba(0,0,0,.8);
    color: #eee;
    padding: 8px;
    font-size: 12px;
    line-height: 1.4;
    border-radius: 6px;
    overflow-y: auto;
    white-space: pre-wrap;
    z-index: 2147483646;
    display: ${logVisible ? 'block' : 'none'};`;
  document.body.appendChild(logPanel);

  /* ---------- 事件绑定 ---------- */
  toggleBtn.addEventListener('click', () => switchEnable(!enabled));

  logBtn.addEventListener('click', () => {
    logVisible = !logVisible;
    GM_setValue('logVisible', logVisible);
    logPanel.style.display = logVisible ? 'block' : 'none';
    logBtn.textContent = logVisible ? '隐藏日志' : '显示日志';
  });

  refreshInput.addEventListener('change', () => {
    const val = parseInt(refreshInput.value, 10);
    if (!isNaN(val) && val >= 500) {
      refreshInterval = val;
      GM_setValue('refreshInterval', refreshInterval);
      log('Refresh interval set to ' + refreshInterval + ' ms');
      if (enabled) {
        stopLoop();
        startLoop(true);
      }
    } else {
      refreshInput.value = refreshInterval;
    }
  });

  /* ---------- 辅助函数 ---------- */
  function baseBtnStyle(bg) {
    return {
      padding: '8px 14px',
      borderRadius: '6px',
      background: bg,
      color: '#fff',
      border: 'none',
      cursor: 'pointer',
      fontSize: '14px',
      boxShadow: '0 2px 6px rgba(0,0,0,.2)',
    };
  }

  function notify(text) {
    if (typeof GM_notification === 'function') {
      GM_notification({ text, title: '学习通抢讲座', timeout: 5000 });
    }
  }

  function log(msg) {
    const line = `[${new Date().toLocaleTimeString()}] ${msg}`;
    console.log('%c[CX-Log] ' + msg, 'color:#03a9f4');
    logPanel.textContent += line + '\n';
    logPanel.scrollTop = logPanel.scrollHeight;
  }

  function switchEnable(state) {
    enabled = state;
    GM_setValue('enabled', enabled);
    toggleBtn.textContent = `自动抢: ${enabled ? 'ON' : 'OFF'}`;
    toggleBtn.style.background = enabled ? '#4caf50' : '#f44336';
    if (enabled) {
      notify('已开启自动抢讲座');
      log('Auto-enroll ENABLED');
      startLoop(true);
    } else {
      notify('已暂停自动抢讲座');
      log('Auto-enroll DISABLED');
      stopLoop();
    }
  }

  /* ---------- 业务逻辑 ---------- */
  let loopID = null;
  let refreshCount = 0;

  function tryEnroll() {
    const btnList = Array.from(document.querySelectorAll('a[class*="255-btn"]'));
    const enrollBtn = btnList.find((b) => b.textContent.includes('报名参与'));
    const signedBtn = btnList.find((b) => b.textContent.includes('去签到'));
    const fullBtn = btnList.find((b) => b.textContent.includes('名额已满'));

    if (signedBtn) {
      notify('🎉 已成功抢到讲座名额,快去签到!');
      log('Got the seat! Stopping.');
      switchEnable(false);
      return;
    }

    if (enrollBtn) {
      log('Found enroll button, clicking…');
      enrollBtn.click();
      notify('🤖 正在尝试报名…');
      setTimeout(() => location.reload(), 1500);
      return;
    }

    if (fullBtn) {
      log('名额已满,触发熔断,停止刷新');
      notify('名额已满,已停止自动抢');
      switchEnable(false);
      return;
    }

    log('No available seat yet (' + ++refreshCount + ')');
  }

  function startLoop(immediate = false) {
    if (loopID !== null) return;
    log('Start loop (interval ' + refreshInterval + ' ms)');

    const intervalFn = () => {
      tryEnroll();
      log('Next reload in ' + refreshInterval + ' ms');
      setTimeout(() => {
        if (enabled) location.reload();
      }, refreshInterval);
    };

    if (immediate) intervalFn();
    loopID = setInterval(intervalFn, refreshInterval);
  }

  function stopLoop() {
    if (loopID !== null) {
      clearInterval(loopID);
      loopID = null;
      log('Loop stopped');
    }
  }

  /* ---------- 初始化 ---------- */
  log(`Script init — enabled=${enabled}, interval=${refreshInterval} ms`);
  if (enabled) startLoop();
})();