// ==UserScript== // @name 西南交大教务系统一键评价助手 // @namespace http://tampermonkey.net/ // @version 3.3 // @description 极速完成所有课程评价 // @author Antigravity // @match https://jwc.swjtu.edu.cn/vatuu/AssessAction?setAction=list* // @match https://jwc.swjtu.edu.cn/vatuu/AssessAction?setAction=viewAssess&sid=* // @match https://jwc.swjtu.edu.cn/vatuu/AssessAction // @grant none // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/558725/%E8%A5%BF%E5%8D%97%E4%BA%A4%E5%A4%A7%E6%95%99%E5%8A%A1%E7%B3%BB%E7%BB%9F%E4%B8%80%E9%94%AE%E8%AF%84%E4%BB%B7%E5%8A%A9%E6%89%8B.user.js // @updateURL https://update.greasyfork.icu/scripts/558725/%E8%A5%BF%E5%8D%97%E4%BA%A4%E5%A4%A7%E6%95%99%E5%8A%A1%E7%B3%BB%E7%BB%9F%E4%B8%80%E9%94%AE%E8%AF%84%E4%BB%B7%E5%8A%A9%E6%89%8B.meta.js // ==/UserScript== (function () { 'use strict'; /** * ====================================== * CONFIGURATION & CONSTANTS * ====================================== */ const CONFIG = { STORAGE_KEY_RUNNING: 'AF_IS_RUNNING', // 'true' or 'false' STORAGE_KEY_QUEUE: 'AF_COURSE_QUEUE', // JSON Array of URLs STORAGE_KEY_STATE: 'AF_CURRENT_STATE', // 'IDLE', 'ATTEMPT_1', 'RETRY' // Timeouts (ms) DELAY_LIST_JUMP: 1000, DELAY_SUBMIT_FAST: 50, DELAY_BACK_CLICK: 500, // Selectors SEL_RADIO_5: 'input[type="radio"][score="5.0"]', SEL_TEXTAREA: 'textarea', SEL_SUBMIT: 'input[value="提交"], button[onclick*="Submit"]', SEL_BACK_LINK: 'a[href*="history.go(-1)"]', // Texts TEXT_ASK_START: "确定要开始全自动极速评价吗?\n脚本将尝试绕过时间限制。", TEXT_ERROR_SIG: "参数错误" }; /** * ====================================== * UTILITIES * ====================================== */ const Utils = { sleep: (ms) => new Promise(res => setTimeout(res, ms)), log: (msg) => { console.log(`[EvalBot] ${msg}`); UI.appendLog(msg); }, State: { isRunning: () => sessionStorage.getItem(CONFIG.STORAGE_KEY_RUNNING) === 'true', setRunning: (val) => sessionStorage.setItem(CONFIG.STORAGE_KEY_RUNNING, val), getQueue: () => JSON.parse(sessionStorage.getItem(CONFIG.STORAGE_KEY_QUEUE) || '[]'), setQueue: (arr) => sessionStorage.setItem(CONFIG.STORAGE_KEY_QUEUE, JSON.stringify(arr)), getStatus: () => sessionStorage.getItem(CONFIG.STORAGE_KEY_STATE) || 'IDLE', setStatus: (val) => sessionStorage.setItem(CONFIG.STORAGE_KEY_STATE, val), reset: () => { sessionStorage.removeItem(CONFIG.STORAGE_KEY_QUEUE); sessionStorage.removeItem(CONFIG.STORAGE_KEY_STATE); sessionStorage.setItem(CONFIG.STORAGE_KEY_RUNNING, 'false'); } }, // Helper to check precise page type PageType: { // Strictly match List page (exclude listOthers, termAppraise etc.) isList: () => location.href.indexOf('setAction=list') !== -1 && location.href.indexOf('setAction=listOthers') === -1, isDetail: () => location.href.indexOf('setAction=viewAssess') !== -1, isError: () => document.body.innerText.includes(CONFIG.TEXT_ERROR_SIG) || (document.querySelector(CONFIG.SEL_BACK_LINK) && !document.querySelector('#answerForm')) } }; /** * ====================================== * UI MODULE * ====================================== */ const UI = { panelId: 'af-smart-panel', init: () => { if (document.getElementById(UI.panelId)) return; const style = document.createElement('style'); style.textContent = ` #${UI.panelId} { position: fixed; top: 80px; right: 20px; width: 220px; background: rgba(255, 255, 255, 0.98); border: 1px solid #1488F5; border-top: 4px solid #1488F5; box-shadow: 0 4px 12px rgba(0,0,0,0.15); border-radius: 4px; padding: 12px; z-index: 999999; font-family: 'Microsoft YaHei', sans-serif; font-size: 13px; } .af-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; border-bottom: 1px solid #eee; padding-bottom: 5px; } .af-title { font-weight: bold; color: #1488F5; } .af-close { cursor: pointer; font-size: 16px; color: #999; line-height: 1; } .af-close:hover { color: #333; } .af-btn { display: block; width: 100%; padding: 6px 0; margin-bottom: 6px; background: #1488F5; color: white; border: none; border-radius: 3px; cursor: pointer; transition: 0.2s; } .af-btn:hover { opacity: 0.9; } .af-btn.stop { background: #e74c3c; } .af-btn.scnd { background: #f8f9fa; color: #333; border: 1px solid #ddd; } #af-log-area { height: 80px; overflow-y: auto; background: #f8f9fa; border: 1px solid #eee; padding: 5px; color: #555; font-size: 11px; margin-top: 5px; text-align: left; } `; document.head.appendChild(style); const div = document.createElement('div'); div.id = UI.panelId; document.body.appendChild(div); }, close: () => { const el = document.getElementById(UI.panelId); if (el) el.style.display = 'none'; }, renderRunning: (queueLen, statusTxt) => { const panel = document.getElementById(UI.panelId); panel.innerHTML = `
🤖 智能运行中 ×
剩余课程: ${queueLen}
状态: ${statusTxt}
`; document.getElementById('af-stop').onclick = () => { Utils.State.reset(); location.reload(); }; document.getElementById('af-close-btn').onclick = UI.close; }, renderIdle: () => { const panel = document.getElementById(UI.panelId); let buttonsHtml = ''; if (Utils.PageType.isList()) { buttonsHtml = ``; } else if (Utils.PageType.isDetail()) { buttonsHtml = ``; } panel.innerHTML = `
✨ 评价助手 ×
${buttonsHtml}
自动完成课程评价
`; if (document.getElementById('af-start')) { document.getElementById('af-start').onclick = () => { if (confirm(CONFIG.TEXT_ASK_START)) { Utils.State.setRunning('true'); sessionStorage.removeItem(CONFIG.STORAGE_KEY_QUEUE); location.reload(); } }; } if (document.getElementById('af-test-one')) { document.getElementById('af-test-one').onclick = function () { // Use full function to access 'this' if needed, or getElement Actions.fillForm(); const btn = document.getElementById('af-test-one'); btn.innerText = "✅ 已填写"; setTimeout(() => btn.innerText = "⚡ 仅填写当前页", 2000); }; } document.getElementById('af-close-btn').onclick = UI.close; }, appendLog: (txt) => { const el = document.getElementById('af-log-area'); if (el) { el.innerHTML = `
> ${txt}
` + el.innerHTML; } } }; /** * ====================================== * CORE ACTIONS * ====================================== */ const Actions = { scanCourses: () => { const links = Array.from(document.querySelectorAll('a[href*="setAction=viewAssess"]')); const uniqueUrls = new Set(); links.forEach(a => { if (a.innerText.includes("填写问卷")) uniqueUrls.add(a.href); }); return Array.from(uniqueUrls); }, fillForm: () => { document.querySelectorAll('.questionDiv, .answerDiv').forEach(d => d.style.display = 'block'); const radios = document.querySelectorAll(CONFIG.SEL_RADIO_5); radios.forEach(r => { if (!r.checked) r.click(); }); const comments = [ "老师授课认真负责,知识点讲解清晰", "课程内容充实,收获良多", "无", "暂无建议" ]; // Q17 (comments[0]), Q18 (comments[2]) if structured like before // Or simple iteration: const textareas = document.querySelectorAll(CONFIG.SEL_TEXTAREA); textareas.forEach((ta, i) => { // If it's the first textarea, positive comment. If second, "None". if (!ta.value) ta.value = i === 0 ? comments[0] : comments[2]; }); return radios.length; }, submitForm: () => { // 提交前确保 allNum 已被正确赋值 // 网页的 allNum 在 window.onload 的 setTimeout 中设置, // 如果脚本执行太早,allNum 仍为 0,会导致验证失败 if (typeof window.allNum !== 'undefined') { const problemIds = document.querySelectorAll('input[name="problem_id"]'); if (problemIds.length > 0 && window.allNum < problemIds.length) { Utils.log(`修正 allNum: ${window.allNum} -> ${problemIds.length}`); window.allNum = problemIds.length; } } if (typeof window.goSubmitForm === 'function') { window.goSubmitForm(); } else { const btn = document.querySelector(CONFIG.SEL_SUBMIT); if (btn) btn.click(); } }, clickBack: () => { const link = document.querySelector(CONFIG.SEL_BACK_LINK); if (link) link.click(); else window.history.go(-1); } }; /** * ====================================== * LOGIC CONTROLLERS * ====================================== */ const Controllers = { onListPage: async () => { // Only show UI if idle or running if (!Utils.State.isRunning()) { UI.init(); UI.renderIdle(); return; } else { UI.init(); } let queue = Utils.State.getQueue(); // Initial Scan if (queue.length === 0) { UI.renderRunning(0, "正在扫描课程..."); queue = Actions.scanCourses(); Utils.State.setQueue(queue); Utils.log(`扫描到 ${queue.length} 门未评课程`); } UI.renderRunning(queue.length, "准备评价下一门..."); if (queue.length > 0) { const nextUrl = queue.shift(); Utils.State.setQueue(queue); Utils.State.setStatus('IDLE'); Utils.log("3秒后跳转..."); await Utils.sleep(CONFIG.DELAY_LIST_JUMP); window.location.href = nextUrl; } else { // Done or Empty Utils.State.reset(); // Try to find the "View Grades" link/button // Usually it's "我已完成全部评价,现在查看成绩" or similar on the list page // Selector based on value attribute of input button const finishBtn = document.querySelector('input[value*="我已完成全部评价"], input[value*="查看成绩"]'); if (finishBtn) { // alert("🎉 全部完成!即将跳转查看成绩..."); // Optional: Remove alert for full automation finishBtn.click(); } else { alert("🎉 全部完成!(未找到自动跳转按钮,请手动查看成绩)"); location.reload(); } } }, onDetailPage: async () => { // Only show UI if idle or running if (!Utils.State.isRunning()) { UI.init(); UI.renderIdle(); return; } else { UI.init(); // Show running state } const queue = Utils.State.getQueue(); const status = Utils.State.getStatus(); UI.renderRunning(queue.length, `填写中 [${status}]`); // 等待页面的 allNum 被正确赋值(window.onload 中的 setTimeout 33ms) // 最多等待 2 秒,每 100ms 检查一次 for (let i = 0; i < 20; i++) { if (typeof window.allNum !== 'undefined' && window.allNum > 0) break; await Utils.sleep(100); } if (typeof window.allNum === 'undefined' || window.allNum === 0) { Utils.log('⚠️ allNum 未初始化,尝试手动设置...'); const problemIds = document.querySelectorAll('input[name="problem_id"]'); if (problemIds.length > 0) { window.allNum = problemIds.length; Utils.log(`手动设置 allNum = ${problemIds.length}`); } } // Phase 1: Fail it if (status === 'IDLE' || status === 'ATTEMPT_1') { Utils.log("第一次提交 (尝试诱发错误)..."); Utils.State.setStatus('ATTEMPT_1'); Actions.fillForm(); await Utils.sleep(CONFIG.DELAY_SUBMIT_FAST); Actions.submitForm(); } // Phase 2: Retry it else if (status === 'RETRY') { Utils.log("检测到返回,第二次提交..."); Actions.fillForm(); await Utils.sleep(CONFIG.DELAY_SUBMIT_FAST); Actions.submitForm(); } }, onErrorPage: async () => { // Error page logic only relevant if we are Running if (!Utils.State.isRunning()) return; UI.init(); // Show UI on error page too if running const status = Utils.State.getStatus(); if (status === 'ATTEMPT_1') { UI.renderRunning(Utils.State.getQueue().length, "⚠️ 捕获错误页"); Utils.log("成功触发限制,正在返回..."); Utils.State.setStatus('RETRY'); await Utils.sleep(CONFIG.DELAY_BACK_CLICK); Actions.clickBack(); } else { Utils.log("未知错误状态,停止运行"); Utils.State.setRunning('false'); } } }; /** * ====================================== * MAIN ROUTER * ====================================== */ function main() { // Strict Page Routing if (Utils.PageType.isError()) { Controllers.onErrorPage(); } else if (Utils.PageType.isDetail()) { Controllers.onDetailPage(); } else if (Utils.PageType.isList()) { Controllers.onListPage(); } } main(); })();