您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
适配新版教务系统的自动选课脚本(可视日志、可暂停刷新、UI增强版)
// ==UserScript== // @name 新海天帮你查课余量 (可视化日志增强版) // @namespace http://tampermonkey.net/ // @version 3.0 // @description 适配新版教务系统的自动选课脚本(可视日志、可暂停刷新、UI增强版) // @author 上条当咩 & Claude & Gemini // @match https://aa.bjtu.edu.cn/course_selection/courseselecttask/selects/ // @icon https://love.nimisora.icu/homework-notify/nimisora.png // @license MIT // @grant GM_xmlhttpRequest // @grant GM_notification // @grant GM_addStyle // ==/UserScript== (function() { 'use strict'; // ------------------- 配置区 ------------------- // 您的愿望单课程数组 - 只需填写课程号和序号,例如: ['M402001B 01', 'A121033B 01'] var wishListCourses = [ 'M402018B 01',//虚拟化 'M402004B 01',// 软工 ]; // 刷新延迟(毫秒),默认为 2000 (2秒) const REFRESH_DELAY = 2000; // ------------------- 配置区结束 ------------------- // --- 全局状态变量 --- let hasSubmitted = false; let notificationIntervals = {}; // 存储每个课程的通知计时器 let isPaused = false; // 是否暂停自动刷新 // ------------------- 可视化日志窗口模块 ------------------- const LogManager = { logWindow: null, logContent: null, showLogButton: null, init: function() { // 1. 注入CSS样式 GM_addStyle(` #log-window-container { position: fixed; bottom: 20px; right: 20px; width: 450px; max-height: 350px; background-color: rgba(255, 255, 255, 0.95); border: 1px solid #ccc; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.2); z-index: 9999; display: flex; flex-direction: column; font-family: 'Microsoft YaHei', sans-serif; font-size: 13px; transition: opacity 0.3s, transform 0.3s; } #log-window-container.hidden { opacity: 0; transform: scale(0.95); pointer-events: none; } #log-header { padding: 8px 12px; background-color: #f0f0f0; border-bottom: 1px solid #ccc; cursor: move; display: flex; justify-content: space-between; align-items: center; user-select: none; border-top-left-radius: 8px; border-top-right-radius: 8px; } #log-header-title { font-weight: bold; color: #333; } #log-content { padding: 10px; overflow-y: auto; flex-grow: 1; color: #333; background-color: #fff; } #log-content p { margin: 0 0 6px 0; padding: 0 0 4px 0; line-height: 1.5; border-bottom: 1px dotted #eee; word-break: break-all; } #log-content .log-success { color: #28a745; font-weight: bold; } #log-content .log-error { color: #dc3545; font-weight: bold; } #log-content .log-warn { color: #f39c12; } #log-content .log-info { color: #007bff; } #log-content .log-check { color: #6c757d; } .log-controls button { margin-left: 8px; padding: 4px 8px; font-size: 12px; cursor: pointer; border: 1px solid #ccc; border-radius: 4px; background-color: #fff; } .log-controls button:hover { background-color: #e9e9e9; border-color: #bbb; } #show-log-button { position: fixed; bottom: 20px; right: 20px; z-index: 9998; display: none; /* Initially hidden */ padding: 8px 12px; cursor: pointer; background-color: #007bff; color: white; border: none; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); } #show-log-button.visible { display: block; } `); // 2. 创建HTML元素 document.body.insertAdjacentHTML('beforeend', ` <div id="log-window-container"> <div id="log-header"> <span id="log-header-title">选课脚本日志</span> <div class="log-controls"> <button id="toggle-pause-btn">暂停刷新</button> <button id="clear-log-btn">清空日志</button> <button id="hide-log-btn">隐藏</button> </div> </div> <div id="log-content"></div> </div> <button id="show-log-button">显示日志</button> `); // 3. 获取DOM引用 this.logWindow = document.getElementById('log-window-container'); this.logContent = document.getElementById('log-content'); this.showLogButton = document.getElementById('show-log-button'); // 4. 绑定事件 this.makeDraggable(document.getElementById('log-header'), this.logWindow); document.getElementById('toggle-pause-btn').addEventListener('click', this.togglePause); document.getElementById('clear-log-btn').addEventListener('click', () => this.logContent.innerHTML = ''); document.getElementById('hide-log-btn').addEventListener('click', () => this.toggleVisibility(false)); this.showLogButton.addEventListener('click', () => this.toggleVisibility(true)); this.log('日志窗口初始化成功', 'success'); }, log: function(message, type = 'info') { if (!this.logContent) return; const time = new Date().toLocaleTimeString(); const logClass = `log-${type}`; const p = document.createElement('p'); p.className = logClass; p.innerHTML = `[${time}] ${message}`; this.logContent.appendChild(p); // 自动滚动到底部 this.logContent.scrollTop = this.logContent.scrollHeight; // 同时在控制台输出,方便调试 console.log(`[Tampermonkey] ${message}`); }, togglePause: function() { isPaused = !isPaused; const btn = document.getElementById('toggle-pause-btn'); btn.textContent = isPaused ? '恢复刷新' : '暂停刷新'; btn.style.color = isPaused ? '#dc3545' : ''; LogManager.log(`刷新已 ${isPaused ? '暂停' : '恢复'}`, 'warn'); }, toggleVisibility: function(show) { if(show) { this.logWindow.classList.remove('hidden'); this.showLogButton.classList.remove('visible'); } else { this.logWindow.classList.add('hidden'); this.showLogButton.classList.add('visible'); } }, makeDraggable: function(header, element) { let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; header.onmousedown = e => { e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = () => { document.onmouseup = null; document.onmousemove = null; }; document.onmousemove = e => { e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; element.style.top = (element.offsetTop - pos2) + "px"; element.style.left = (element.offsetLeft - pos1) + "px"; }; }; } }; // ------------------- 原有脚本逻辑(已集成日志功能) ------------------- // 发送循环通知 function startRepeatingNotification(courseCode) { if (notificationIntervals[courseCode]) return; LogManager.log(`为课程 ${courseCode} 开启循环通知`, 'warn'); notificationIntervals[courseCode] = setInterval(() => { GM_notification({ title: '课程余量提醒!', text: `课程 ${courseCode} 有余量!点击此通知停止该课程的提醒`, timeout: 0, onclick: () => stopNotification(courseCode) }); }, 500); // 每0.5秒发送一次通知 } // 停止特定课程的通知 function stopNotification(courseCode) { if (notificationIntervals[courseCode]) { clearInterval(notificationIntervals[courseCode]); delete notificationIntervals[courseCode]; LogManager.log(`已停止 ${courseCode} 的通知`, 'info'); } } // 停止所有通知 function stopAllNotifications() { Object.keys(notificationIntervals).forEach(stopNotification); } // 从课程描述中提取课程信息 function extractCourseInfo(courseCell) { const ellipsisElement = courseCell.querySelector('.ellipsis'); if (!ellipsisElement) return null; const description = ellipsisElement.getAttribute('title'); if (!description) return null; const regex = /([A-Z]\d{6}[A-Z]):.*?(\d{2})/; const match = description.match(regex); if (match) { return { courseCode: match[1], sectionNum: match[2], fullCode: `${match[1]} ${match[2]}` }; } return null; } // 点击提交按钮 function clickSubmitButton() { var submitButton = document.getElementById('select-submit-btn'); if (submitButton) { submitButton.click(); LogManager.log('提交按钮已点击', 'info'); return true; } LogManager.log('提交按钮未找到', 'error'); return false; } // 处理验证码 function handleCaptcha() { var captchaDialog = document.querySelector('.captcha-dialog:not(.hide)'); if (captchaDialog) { var inputField = captchaDialog.querySelector('input[name="answer"]'); if (inputField) { LogManager.log('检测到验证码,请输入后按下回车键提交', 'warn'); return true; } } return false; } // 点击确认按钮 function clickConfirmButton() { var confirmButton = document.querySelector('.btn-info[data-bb-handler="ok"]'); if (confirmButton) { confirmButton.click(); LogManager.log('最终确认按钮已点击,选课成功!', 'success'); stopAllNotifications(); return true; } return false; } // 点击复选框并处理"已了解"模态框 function clickCheckboxAndUnderstandModal(courseCode, fullCode) { var checkbox = document.querySelector(`input[name="checkboxs"][kch="${courseCode}"]`); if (checkbox && !checkbox.disabled) { checkbox.click(); LogManager.log(`已为课程 ${fullCode} 勾选复选框`, 'info'); setTimeout(() => { const understandButton = document.querySelector('.btn[data-bb-handler="info"]'); if (understandButton) { understandButton.click(); LogManager.log(`已自动确认 ${fullCode} 的“已了解”提示`, 'info'); } }, 500); } } // 提交选课 function submit() { if (clickSubmitButton()) { hasSubmitted = true; setTimeout(() => { if (handleCaptcha()) { document.addEventListener('keydown', function(event) { if (event.key === 'Enter') { clickConfirmButton(); } }); } else { // 如果没有验证码,可能直接弹出成功/失败窗口 clickConfirmButton(); } }, 1000); // 等待一下,让验证码或结果对话框出现 } } // 主要逻辑 function main() { LogManager.log(`开始扫描愿望单课程: [${wishListCourses.join(', ')}]`, 'info'); const courseTable = document.querySelector('#current table'); if (!courseTable) { LogManager.log('未找到课程表,可能是页面结构已改变。', 'error'); return; } const rows = courseTable.querySelectorAll('tbody tr'); let availableCourseCount = 0; let foundWishListCourses = []; rows.forEach(row => { const cells = row.cells; if (cells.length < 2) return; const courseInfo = extractCourseInfo(cells[1]); if (courseInfo && wishListCourses.includes(courseInfo.fullCode)) { const statusText = cells[0].textContent.trim(); LogManager.log(`检查课程: ${courseInfo.fullCode}, 状态: ${statusText}`, 'check'); foundWishListCourses.push(courseInfo.fullCode); if (!statusText.includes('无余量') && !statusText.includes('已选')) { LogManager.log(`发现课程 ${courseInfo.fullCode} 有余量!`, 'success'); availableCourseCount++; if (!hasSubmitted) { clickCheckboxAndUnderstandModal(courseInfo.courseCode, courseInfo.fullCode); } startRepeatingNotification(courseInfo.fullCode); } } }); // 检查是否有愿望单中的课程未在页面上找到 wishListCourses.forEach(c => { if (!foundWishListCourses.includes(c)) { LogManager.log(`警告:愿望单课程 ${c} 未在当前页面找到,请检查课程号和选课轮次是否正确。`, 'error'); } }); if (availableCourseCount > 0 && !hasSubmitted) { LogManager.log(`共发现 ${availableCourseCount} 门可选课程,准备提交...`, 'warn'); submit(); } else if (availableCourseCount === 0) { LogManager.log('愿望单中无有余量的课程,准备刷新...'); if (!isPaused) { setTimeout(() => { LogManager.log('正在刷新页面...', 'info'); location.reload(); }, REFRESH_DELAY); } else { LogManager.log('刷新已暂停,脚本将不执行任何操作。', 'warn'); } } } // 页面卸载时清理所有通知 window.addEventListener('beforeunload', () => { stopAllNotifications(); }); // 启动脚本 // 使用 setTimeout 确保页面元素完全加载 setTimeout(() => { LogManager.init(); // 初始化日志窗口 main(); // 运行主逻辑 }, 500); })();