// ==UserScript== // @name 黑龙江大学自动评教 // @namespace https://github.com/auto-fuck // @version 1.2 // @description 自动完成黑龙江大学教育质量监控系统的学生评教问卷 // @author auto // @match https://zlpj.hlju.edu.cn/* // @grant none // @run-at document-idle // ==/UserScript== (function () { 'use strict'; const CONFIG = { fillDelay: 800, submitDelay: 1000, pollInterval: 1500, defaultText: '很好', mode: 'manual', // 'manual'(填完暂停等用户核验提交) | 'auto'(全自动) }; const STATE = { running: false, totalCount: 0, doneCount: 0, loopActive: false, // 防止 processListLoop 重入 }; const LOG = '[自动评教]'; function log(...a) { console.log(LOG, ...a); } function toast(msg, dur = 2000) { const el = document.createElement('div'); el.textContent = msg; el.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.8);color:#fff;padding:10px 20px;border-radius:5px;z-index:2147483647;font-size:14px;white-space:nowrap;'; document.body.appendChild(el); setTimeout(() => el.remove(), dur); } function delay(ms) { return new Promise(r => setTimeout(r, ms)); } function detectPage() { const h = window.location.hash; if (h.includes('/survery/children/surverySubmit/')) return 'survey'; if (h.includes('/survery')) return 'list'; return 'other'; } function isVisible(el) { if (!el) return false; const s = window.getComputedStyle(el); return s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0'; } function triggerVueInput(el) { el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } // ==================== 弹窗自动确认 ==================== function startDialogWatcher() { const observer = new MutationObserver(() => { if (!STATE.running) return; // Vux confirm dialog - 自动点击"确定" const confirmBtns = document.querySelectorAll('.weui-dialog__btn_primary'); for (const btn of confirmBtns) { if (isVisible(btn) && btn.textContent.includes('确定')) { log('自动确认弹窗'); btn.click(); } } // Vux alert - 自动关闭 const alertBtns = document.querySelectorAll('.vux-alert .weui-dialog__btn_primary'); for (const btn of alertBtns) { if (isVisible(btn)) { log('自动关闭提示弹窗'); btn.click(); } } // sweetalert2 const swalConfirm = document.querySelector('.swal2-confirm'); if (swalConfirm && isVisible(swalConfirm)) { log('自动确认 swal 弹窗'); swalConfirm.click(); } }); observer.observe(document.body, { childList: true, subtree: true }); return observer; } // ==================== 列表页:获取第一个待评教条目 ==================== function getFirstPendingItem() { const items = document.querySelectorAll('.survery .item.bottom'); for (const item of items) { // 没有 icon-defen(评分图标)= 未完成 if (!item.querySelector('.icon-defen')) { return item; } } return null; } function countPendingItems() { let count = 0; const items = document.querySelectorAll('.survery .item.bottom'); for (const item of items) { if (!item.querySelector('.icon-defen')) count++; } return count; } // ==================== 列表页:处理循环 ==================== async function processListLoop() { if (STATE.loopActive) return; STATE.loopActive = true; try { while (STATE.running) { await delay(CONFIG.pollInterval); if (detectPage() !== 'list') { STATE.loopActive = false; return; } const pendingCount = countPendingItems(); if (pendingCount === 0) { log('所有问卷已完成!'); toast('所有评教问卷已完成!'); STATE.running = false; setStatus('已完成', '#7cba23'); STATE.loopActive = false; return; } if (STATE.totalCount === 0) { STATE.totalCount = pendingCount; STATE.doneCount = 0; log(`找到 ${pendingCount} 个待完成问卷`); toast(`找到 ${pendingCount} 个待完成问卷,开始自动评教...`); } updateProgress(); const item = getFirstPendingItem(); if (!item) continue; const titleEl = item.querySelector('.title'); log(`处理: ${titleEl ? titleEl.textContent.trim() : '未知问卷'}`); setStatus('填写中...', '#7cba23'); item.click(); // 等待离开列表页 let retry = 0; while (detectPage() === 'list' && retry < 20) { await delay(500); retry++; } if (detectPage() === 'survey') { await handleSurveyPage(); } // 等待返回列表页(手动模式下由用户控制提交时机) if (CONFIG.mode === 'manual') { setStatus('等待核验提交...', '#ff9800'); } retry = 0; while (STATE.running && retry < 300) { // 5分钟超时 await delay(1000); retry++; if (detectPage() === 'list') { const first = getFirstPendingItem(); if (first || countPendingItems() === 0) { await delay(1500); break; } } } setStatus('运行中...', '#7cba23'); await delay(1500); } } finally { STATE.loopActive = false; } } // ==================== 问卷页 ==================== async function handleSurveyPage() { log('问卷页:等待加载...'); await delay(CONFIG.fillDelay); for (let attempt = 0; attempt < 10; attempt++) { if (await tryFillAndSubmit()) break; await delay(1000); } } async function tryFillAndSubmit() { const filled = await fillAllQuestions(); if (!filled) return false; log('填写完成,准备提交...'); if (CONFIG.mode === 'manual') { toast('填写完成,请核验后手动提交'); setStatus('等待核验提交...', '#ff9800'); return true; } await delay(CONFIG.submitDelay); // 查找提交按钮 const allBtns = document.querySelectorAll('button, a.weui-btn, .x-button, [role="button"], .submitbtn'); for (const btn of allBtns) { if (!isVisible(btn)) continue; const txt = (btn.textContent || '').trim(); if (txt === '提交' || txt === '暂存' || txt === '保存') { log('点击提交按钮:', txt); btn.click(); STATE.doneCount++; updateProgress(); return true; } } log('未找到提交按钮'); return false; } async function fillAllQuestions() { try { fillRadios(); fillSelects(); fillTextFields(); fillSortInputs(); fillCheckboxes(); await delay(200); return true; } catch (e) { log('填写出错:', e); return false; } } // 选项顺序:第一个=最优(5分/是),最后=最差(1分/否) function pickOptionIndex(count) { if (count === 2) return 0; // 二选项:选"是"(第一个) if (count === 5) return Math.random() < 0.5 ? 0 : 1; // 五选项:index 0=5分 或 index 1=4分 return 0; // 其他:选最优(第一个) } function fillRadios() { const groups = new Map(); const radios = document.querySelectorAll('input[type="radio"]'); for (const r of radios) { const name = r.getAttribute('name'); if (!name) continue; if (!groups.has(name)) groups.set(name, []); groups.get(name).push(r); } for (const [, group] of groups) { if (group.some(r => r.checked)) continue; const idx = pickOptionIndex(group.length); group[idx].click(); } } function fillSelects() { const selects = document.querySelectorAll('select'); for (const sel of selects) { const count = sel.options.length; if (count > 1) { sel.selectedIndex = pickOptionIndex(count); triggerVueInput(sel); } } } function fillTextFields() { const inputs = document.querySelectorAll( 'input:not([type="radio"]):not([type="checkbox"]):not([type="hidden"]):not([type="submit"]):not([type="button"])' ); for (const inp of inputs) { if (inp.readOnly) continue; if (inp.classList.contains('matrix_sort') || inp.classList.contains('sortnum')) continue; if (!inp.value || inp.value.trim() === '') { inp.value = CONFIG.defaultText; triggerVueInput(inp); } } const textareas = document.querySelectorAll('textarea'); for (const ta of textareas) { if (!ta.value || ta.value.trim() === '') { ta.value = CONFIG.defaultText; triggerVueInput(ta); } } } function fillSortInputs() { const sortInputs = document.querySelectorAll('.matrix_sort, .sortnum'); for (let i = 0; i < sortInputs.length; i++) { if (!sortInputs[i].value || sortInputs[i].value === '') { sortInputs[i].value = String(i + 1); sortInputs[i].classList.add('sortnum-sel'); triggerVueInput(sortInputs[i]); } } } function fillCheckboxes() { const checkboxes = document.querySelectorAll('input[type="checkbox"]:not(:checked)'); for (const cb of checkboxes) { if ((cb.id || '').startsWith('ae-')) continue; cb.click(); } } function updateProgress() { const el = document.getElementById('ae-progress'); if (el) { el.style.display = 'block'; el.textContent = `进度:${STATE.doneCount} / ${STATE.totalCount}`; } } // ==================== 控制面板 ==================== function createPanel() { const panel = document.createElement('div'); panel.id = 'auto-eval-panel'; panel.innerHTML = `
自动评教 ×
状态:待命中
快捷键: Ctrl+Shift+A
`; document.body.appendChild(panel); document.getElementById('ae-close').onclick = () => { panel.style.display = 'none'; }; document.getElementById('ae-start').onclick = start; document.getElementById('ae-stop').onclick = stop; document.getElementById('ae-auto-submit').onchange = function () { CONFIG.mode = this.checked ? 'auto' : 'manual'; }; } function setStatus(msg, color = '#999') { const el = document.getElementById('ae-status'); if (el) { el.textContent = '状态:' + msg; el.style.color = color; } } async function start() { if (STATE.running) { toast('已在运行中...'); return; } STATE.running = true; STATE.totalCount = 0; STATE.doneCount = 0; setStatus('运行中...', '#7cba23'); const page = detectPage(); log('开始,当前页面:', page); try { if (page === 'list') { await processListLoop(); } else if (page === 'survey') { await handleSurveyPage(); // 等返回列表后继续 await waitForListPage(); if (STATE.running) await processListLoop(); } else { toast('请先进入评教列表页面'); STATE.running = false; setStatus('待命中'); } } catch (e) { log('出错:', e); toast('出错: ' + e.message); STATE.running = false; setStatus('出错', 'red'); } if (!STATE.running) setStatus('待命中'); } async function waitForListPage() { for (let i = 0; i < 90; i++) { if (detectPage() === 'list') { await delay(2000); return; } await delay(1000); } } function stop() { STATE.running = false; setStatus('已停止'); toast('已停止'); } // ==================== 初始化 ==================== function init() { log('脚本已加载, 当前页面:', detectPage()); createPanel(); // hashchange 监听:自动跳回列表时继续处理 let lastHash = window.location.hash; window.addEventListener('hashchange', () => { const h = window.location.hash; if (h === lastHash) return; lastHash = h; if (!STATE.running) return; const page = detectPage(); log('路由变化 ->', page); if (page === 'survey') { handleSurveyPage(); } else if (page === 'list') { updateProgress(); setStatus('运行中...', '#7cba23'); setTimeout(() => { if (STATE.running) processListLoop(); }, 1500); } }); startDialogWatcher(); document.addEventListener('keydown', e => { if (e.ctrlKey && e.shiftKey && e.key === 'A') { e.preventDefault(); STATE.running ? stop() : start(); } }); toast('自动评教脚本已就绪 | Ctrl+Shift+A'); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();