// ==UserScript== // @name 一键评教(蚌医版) // @namespace http://tampermonkey.net/ // @version 1.0 // @description 教学评价一键批量打分,自动逐门评价并提交 // @author jasminenJohn // @license MIT // @match *://byjw.bbmu.edu.cn/* // @grant none // @run-at document-idle // ==/UserScript== (function () { 'use strict'; var CFG = { stateKey: 'bbmuPJ.state', pulseKey: 'bbmuPJ.pulse', aliveKey: 'bbmuPJ.alive', clickDelay: 1200, fillDelay: 900, submitDelay: 900, reloadDelay: 1800, submitTimeout: 25000, maxRounds: 300, grades: ['优', '良', '中', '差'], secondRate: 0.25 }; function loadState() { try { return JSON.parse(localStorage.getItem(CFG.stateKey)) || {}; } catch (e) { return {}; } } function saveState(patch) { var s = Object.assign(loadState(), patch); localStorage.setItem(CFG.stateKey, JSON.stringify(s)); return s; } var state = Object.assign({ running: false, mode: 'random', done: 0, rounds: 0 }, loadState()); function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); } function txt(el) { return (el.textContent || el.value || '').replace(/\s/g, ''); } function isVisible(el) { var r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; } function findEvalLinks() { var nodes = document.querySelectorAll('a, button, span[onclick], td[onclick]'); return Array.prototype.filter.call(nodes, function (el) { return txt(el) === '评价' && isVisible(el); }); } function collectRadioGroups() { var radios = Array.prototype.filter.call( document.querySelectorAll('input[type=radio]'), isVisible); var groups = {}; radios.forEach(function (r) { var key = r.name || 'g' + Math.random(); (groups[key] = groups[key] || []).push(r); }); return Object.keys(groups).map(function (k) { return groups[k]; }) .filter(function (g) { return g.length >= 2; }); } function isListPage() { if (!findEvalLinks().length) return false; var bt = document.body ? document.body.innerText : ''; return /提交教学评价表|评价轮次|评价类型/.test(bt); } function isFormPage() { if (collectRadioGroups().length < 3) return false; var bt = document.body ? document.body.innerText : ''; return bt.indexOf('优') > -1 && bt.indexOf('良') > -1 && bt.indexOf('差') > -1; } function fireEvents(el) { ['input', 'change'].forEach(function (t) { el.dispatchEvent(new Event(t, { bubbles: true })); }); } function radioGradeText(r) { if (r.id) { var lab = document.querySelector('label[for="' + CSS.escape(r.id) + '"]'); if (lab) return txt(lab); } var wrap = r.closest('label'); if (wrap) return txt(wrap); var t = '', n = r.nextSibling; while (n && t.length < 4) { t += (n.textContent || '').trim(); n = n.nextSibling; } if (t) return t; return r.parentElement ? txt(r.parentElement) : ''; } function sortByGrade(group) { function rank(r) { var t = radioGradeText(r); for (var i = 0; i < CFG.grades.length; i++) { if (t.indexOf(CFG.grades[i]) > -1) return i; } return 99; } var ranked = group.map(function (r) { return { r: r, k: rank(r) }; }); if (ranked.every(function (x) { return x.k === 99; })) return group; return ranked.sort(function (a, b) { return a.k - b.k; }) .map(function (x) { return x.r; }); } function pickRadio(group, mode) { var sorted = sortByGrade(group); var target = sorted[0]; if (mode === 'random' && sorted.length > 1 && Math.random() < CFG.secondRate) { target = sorted[1]; } target.checked = true; fireEvents(target); target.click(); } function fillForm(mode) { var groups = collectRadioGroups(); groups.forEach(function (g) { pickRadio(g, mode); }); Array.prototype.forEach.call(document.querySelectorAll('textarea'), function (ta) { if (isVisible(ta) && !ta.value.trim()) { ta.value = '老师教学认真负责,讲解清晰,收获很大。'; fireEvents(ta); } }); return groups.length; } function findSubmitBtn() { var cands = Array.prototype.filter.call( document.querySelectorAll('button, input[type=button], input[type=submit], a'), function (el) { return isVisible(el) && !el.disabled; }); var exact = cands.filter(function (el) { return txt(el) === '提交'; })[0]; if (exact) return exact; return cands.filter(function (el) { return /^(确认提交|保存并提交)$/.test(txt(el)); })[0] || null; } function armAutoConfirm() { try { window.confirm = function () { return true; }; } catch (e) {} try { window.alert = function () {}; } catch (e) {} var tries = 0; var timer = setInterval(function () { var sels = ['.layui-layer-btn0', '.bootbox .btn-primary', '.modal.in .btn-primary', '.modal.show .btn-primary']; for (var i = 0; i < sels.length; i++) { var b = document.querySelector(sels[i]); if (b && isVisible(b)) { b.click(); clearInterval(timer); return; } } if (++tries > 12) clearInterval(timer); }, 400); } function sendPulse() { localStorage.setItem(CFG.pulseKey, JSON.stringify({ t: Date.now() })); } var formHandled = false; function autoFillAndSubmit() { if (formHandled) return; formHandled = true; sleep(CFG.fillDelay).then(function () { if (!loadState().running) { formHandled = false; return; } fillForm(loadState().mode); return sleep(CFG.submitDelay).then(function () { if (!loadState().running) return; var btn = findSubmitBtn(); if (!btn) { saveState({ running: false, error: '找不到提交按钮' }); return; } armAutoConfirm(); saveState({ done: (loadState().done || 0) + 1 }); btn.click(); sendPulse(); }); }); } var watchdog = null; function clearWatchdog() { if (watchdog) { clearTimeout(watchdog); watchdog = null; } } function clickNext() { state = loadState(); if (!state.running) return; var links = findEvalLinks(); setProgress('剩余 ' + links.length + ' 门 · 已完成 ' + state.done + ' 门'); if (!links.length) { saveState({ running: false }); setProgress('🎉 全部完成!共评价 ' + state.done + ' 门'); renderRunning(); return; } if (state.rounds >= CFG.maxRounds) { saveState({ running: false }); setProgress('已达安全上限,自动停止'); renderRunning(); return; } saveState({ rounds: state.rounds + 1 }); sleep(CFG.clickDelay).then(function () { if (!loadState().running) return; setProgress('正在评价第 ' + (loadState().done + 1) + ' 门…'); links[0].click(); clearWatchdog(); watchdog = setTimeout(function () { if (loadState().running) { saveState({ running: false }); setProgress('等待提交超时,已停止'); renderRunning(); } }, CFG.submitTimeout); }); } function onPulse() { clearWatchdog(); setProgress('第 ' + loadState().done + ' 门已提交 ✓ 刷新列表…'); setTimeout(function () { if (loadState().running) location.reload(); }, CFG.reloadDelay); } function startRun() { saveState({ running: true, done: 0, rounds: 0, error: '' }); state = loadState(); renderRunning(); clickNext(); } function stopRun() { saveState({ running: false }); clearWatchdog(); renderRunning(); setProgress('已停止'); } var UI_ID = 'bbmu-pj-panel'; var $panel, $go, $progress; var pageRole = null; function injectStyles() { if (document.getElementById(UI_ID + '-style')) return; var P = '#' + UI_ID; var css = [ P + ' {', ' --blue: #0A84FF; --blue-press: #0060DF;', ' --bg: rgba(255,255,255,0.72);', ' --text: #1d1d1f; --text2: #6e6e73;', ' --field: rgba(118,118,128,0.12);', ' position: fixed; right: 24px; bottom: 24px; z-index: 2147483646;', ' width: 280px; padding: 16px;', ' background: var(--bg);', ' -webkit-backdrop-filter: saturate(180%) blur(24px); backdrop-filter: saturate(180%) blur(24px);', ' border: 0.5px solid rgba(0,0,0,0.08);', ' border-radius: 20px;', ' box-shadow: 0 16px 48px rgba(0,0,0,0.18), 0 2px 8px rgba(0,0,0,0.08);', ' font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", "Helvetica Neue", sans-serif;', ' color: var(--text); font-size: 13px; line-height: 1.45;', '}', '@media (prefers-color-scheme: dark) {', ' ' + P + ' {', ' --bg: rgba(28,28,30,0.8);', ' --text: #f5f5f7; --text2: #98989d;', ' --field: rgba(118,118,128,0.24);', ' border-color: rgba(255,255,255,0.1);', ' }', '}', P + '.pj-collapsed .pj-body { display: none; }', P + ' * { box-sizing: border-box; margin: 0; }', P + ' .pj-head { display: flex; align-items: center; gap: 10px; cursor: grab; user-select: none; }', P + ' .pj-head:active { cursor: grabbing; }', P + ' .pj-icon {', ' width: 32px; height: 32px; border-radius: 8px; flex: none;', ' background: linear-gradient(160deg, #34C759, #30B0C7);', ' display: flex; align-items: center; justify-content: center;', ' color: #fff; font-size: 16px; box-shadow: inset 0 -1px 2px rgba(0,0,0,.15);', '}', P + ' .pj-title { font-size: 15px; font-weight: 600; letter-spacing: -0.2px; }', P + ' .pj-sub { color: var(--text2); font-size: 11px; }', P + ' .pj-chev { margin-left: auto; color: var(--text2); font-size: 12px; transition: transform .3s; }', P + '.pj-collapsed .pj-chev { transform: rotate(180deg); }', P + ' .pj-body { margin-top: 13px; display: flex; flex-direction: column; gap: 11px; }', P + ' .pj-label { font-size: 11px; font-weight: 600; color: var(--text2); text-transform: uppercase; letter-spacing: .4px; margin-bottom: 5px; }', P + ' .pj-seg { display: flex; background: var(--field); border-radius: 9px; padding: 2px; }', P + ' .pj-seg button {', ' flex: 1; border: 0; background: transparent; border-radius: 7px;', ' padding: 6px 0; font: inherit; font-size: 12.5px; color: var(--text);', ' cursor: pointer; transition: all .2s;', '}', P + ' .pj-seg button.on { background: #fff; box-shadow: 0 1px 4px rgba(0,0,0,.12); font-weight: 600; }', '@media (prefers-color-scheme: dark) { ' + P + ' .pj-seg button.on { background: #636366; } }', P + ' .pj-go {', ' width: 100%; border: 0; border-radius: 12px; padding: 11px 0;', ' background: var(--blue); color: #fff; font: inherit; font-size: 14.5px; font-weight: 600;', ' cursor: pointer; letter-spacing: -0.1px; transition: transform .12s, background .2s;', '}', P + ' .pj-go:active { transform: scale(.97); background: var(--blue-press); }', P + ' .pj-go.stop { background: #FF3B30; }', P + ' .pj-progress { text-align: center; color: var(--text2); font-size: 11.5px; min-height: 15px; }', P + ' .pj-foot { text-align: center; color: var(--text2); font-size: 10px; opacity: .7; }' ].join('\n'); var style = document.createElement('style'); style.id = UI_ID + '-style'; style.textContent = css; document.head.appendChild(style); } function makeDraggable(panel, handle) { var sx = 0, sy = 0, px = 0, py = 0, moved = false, dragging = false; handle.addEventListener('pointerdown', function (e) { dragging = true; moved = false; sx = e.clientX; sy = e.clientY; var r = panel.getBoundingClientRect(); px = r.left; py = r.top; handle.setPointerCapture(e.pointerId); }); handle.addEventListener('pointermove', function (e) { if (!dragging) return; var dx = e.clientX - sx, dy = e.clientY - sy; if (!moved && Math.abs(dx) < 4 && Math.abs(dy) < 4) return; moved = true; var x = Math.min(Math.max(px + dx, 8), window.innerWidth - panel.offsetWidth - 8); var y = Math.min(Math.max(py + dy, 8), window.innerHeight - 50); panel.style.left = x + 'px'; panel.style.top = y + 'px'; panel.style.right = 'auto'; panel.style.bottom = 'auto'; }); handle.addEventListener('pointerup', function (e) { dragging = false; handle.releasePointerCapture(e.pointerId); if (!moved) panel.classList.toggle('pj-collapsed'); }); } function buildPanel(role) { if (document.getElementById(UI_ID)) return; injectStyles(); var hint = role === 'list' ? '随机优良 = 大部分优 + 少量良,更自然' : '当前是评价表页,点按钮只处理本页'; $panel = document.createElement('div'); $panel.id = UI_ID; $panel.innerHTML = '
' + '
' + '
' + '
一键评教
' + '
' + (role === 'list' ? '批量自动打分 · 可拖动' : '本页打分 · 可拖动') + '
' + '
' + '
' + '
' + '
' + '
' + '
评分模式
' + '
' + '' + '' + '
' + '
' + '' + '
' + '
' + hint + '
' + '
'; document.body.appendChild($panel); $go = $panel.querySelector('.pj-go'); $progress = $panel.querySelector('.pj-progress'); var segBtns = $panel.querySelectorAll('.pj-seg button'); Array.prototype.forEach.call(segBtns, function (b) { b.classList.toggle('on', b.dataset.mode === state.mode); b.addEventListener('click', function () { saveState({ mode: b.dataset.mode }); state = loadState(); Array.prototype.forEach.call(segBtns, function (x) { x.classList.toggle('on', x === b); }); }); }); makeDraggable($panel, $panel.querySelector('.pj-head')); $go.addEventListener('click', function () { state = loadState(); if (role === 'list') { if (state.running) stopRun(); else startRun(); } else { var n = fillForm(state.mode); setProgress('已打分 ' + n + ' 题'); var btn = findSubmitBtn(); if (btn) { armAutoConfirm(); btn.click(); } else setProgress('已打分 ' + n + ' 题,请手动提交'); } }); renderRunning(); } function renderRunning() { if (!$go || pageRole !== 'list') return; var running = loadState().running; $go.textContent = running ? '停止' : '开始批量评价'; $go.classList.toggle('stop', running); } function setProgress(msg) { if ($progress) $progress.textContent = msg; } var booted = false; function startHeartbeat() { localStorage.setItem(CFG.aliveKey, String(Date.now())); setInterval(function () { localStorage.setItem(CFG.aliveKey, String(Date.now())); }, 1500); } function boot() { if (booted) return; state = Object.assign(state, loadState()); if (isListPage()) { booted = true; pageRole = 'list'; startHeartbeat(); buildPanel('list'); window.addEventListener('storage', function (e) { if (e.key === CFG.pulseKey && loadState().running) onPulse(); }); if (state.running) { setProgress('继续自动评价…'); setTimeout(clickNext, 1600); } return; } if (isFormPage()) { booted = true; pageRole = 'form'; startHeartbeat(); if (loadState().running) { autoFillAndSubmit(); } else { buildPanel('form'); } } } function mountBadge() { if (window !== window.top || document.getElementById(UI_ID + '-badge')) return; var badge = document.createElement('div'); badge.id = UI_ID + '-badge'; badge.style.cssText = 'position:fixed;right:24px;bottom:24px;z-index:2147483645;' + 'padding:9px 14px;border-radius:999px;' + 'background:rgba(28,28,30,0.82);color:#fff;' + 'font:12px -apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif;' + 'box-shadow:0 8px 24px rgba(0,0,0,.25);' + '-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);' + 'pointer-events:none;letter-spacing:.2px'; badge.textContent = '✓ 一键评教已加载 · 等待评价列表页…'; (document.body || document.documentElement).appendChild(badge); var started = Date.now(); var timer = setInterval(function () { var alive = Number(localStorage.getItem(CFG.aliveKey) || 0); if (Date.now() - alive < 5000) { badge.remove(); clearInterval(timer); return; } if (Date.now() - started > 60000) { badge.textContent = '一键评教:未找到评价列表,请进入 教学评价 → 提交教学评价表'; clearInterval(timer); setTimeout(function () { badge.remove(); }, 8000); } }, 1000); } setTimeout(mountBadge, 500); var attempts = 0; var bootTimer = setInterval(function () { if (!booted) boot(); if (booted || ++attempts > 60) clearInterval(bootTimer); }, 1000); setTimeout(boot, 300); })();