// ==UserScript== // @name HLJU 自动评教与学习体验调查 // @namespace https://github.com/peng/hlju-auto-eval // @version 0.1.0 // @description 自动勾选满分、填充主观题,并可在课程列表页批量处理“学习体验调查”和“学生评教”。具有随机评价,避免完全选项相同 // @author peng // @match http://jxpj.hlju.edu.cn/index.php?c=index&a=pager* // @match http://jxpj.hlju.edu.cn/index.php?c=index&a=pagerList* // @run-at document-idle // @grant none // ==/UserScript== (function () { 'use strict'; const QKEY = 'hlju_eval_queue_v1'; const RUN_KEY = 'hlju_eval_running_v1'; const LAST_BUILT_KEY = 'hlju_eval_last_built_term'; const CURRENT_KEY = 'hlju_eval_current_v1'; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); function $(sel, root = document) { return root.querySelector(sel); } function $all(sel, root = document) { return Array.from(root.querySelectorAll(sel)); } function onListPage() { const p = new URLSearchParams(location.search); return p.get('a') === 'pagerList'; } function onPagerPage() { const p = new URLSearchParams(location.search); return p.get('a') === 'pager'; } function getPagerId() { const el = document.querySelector('input[name="pagerId"]'); return el ? el.value : null; // 115: 学习体验调查, 119: 学生评教 } function getQueue() { try { const raw = localStorage.getItem(QKEY); if (!raw) return []; const arr = JSON.parse(raw); if (Array.isArray(arr)) return arr; } catch (_) { /* ignore */ } return []; } function setQueue(arr) { localStorage.setItem(QKEY, JSON.stringify(arr)); } function isRunning() { return localStorage.getItem(RUN_KEY) === '1'; } function setRunning(v) { localStorage.setItem(RUN_KEY, v ? '1' : '0'); } function normalizeUrl(url) { try { const u = new URL(url, location.origin); // 只保留关键参数做去重,保留路由所需参数 const keep = ['c', 'a', 'id', 'kcdm', 'kcxh', 'zgh', 'term']; const p = new URLSearchParams(); for (const k of keep) { const v = u.searchParams.get(k); if (v) p.set(k, v); } // 若是从列表页收集到的 pager 链接,通常包含 c=index&a=pager // 若缺失,则补齐(后端可能依赖该路由) if (!p.get('c')) p.set('c', 'index'); if (!p.get('a') && p.get('id')) p.set('a', 'pager'); return `${u.origin}${u.pathname}?${p.toString()}`; } catch { return url; } } function buildQueueFromListPage() { // 收集所有未标记“已完成”的链接,避免漏掉仍待处理的问卷 const anchors = $all('a[href*="index.php?c=index&a=pager"]'); const pending = anchors.filter(a => !/已完成/.test(a.textContent || '')); const urls = pending.map(a => normalizeUrl(a.href)); const uniq = Array.from(new Set(urls)); setQueue(uniq); const termSel = document.querySelector('select[name="term"]'); const term = termSel ? termSel.value : ''; if (term) localStorage.setItem(LAST_BUILT_KEY, term); return uniq; } function createFloatPanel() { const panel = document.createElement('div'); panel.style.cssText = [ 'position:fixed', 'right:16px', 'bottom:16px', 'z-index:999999', 'background:#1f6feb', 'color:#fff', 'padding:8px 12px', 'border-radius:6px', 'box-shadow:0 4px 12px rgba(0,0,0,.15)', 'font-size:14px', 'line-height:18px', 'cursor:pointer', 'user-select:none' ].join(';'); const btn = document.createElement('span'); const status = () => isRunning() ? '停止自动评教' : '开始自动评教'; btn.textContent = status(); panel.title = '点击切换自动评教'; panel.addEventListener('click', () => { setRunning(!isRunning()); btn.textContent = status(); if (isRunning()) { // 立即触发一次调度 scheduleNextFromList(); } }); panel.appendChild(btn); document.body.appendChild(panel); } async function scheduleNextFromList() { if (!onListPage() || !isRunning()) return; let q = getQueue(); if (!q.length) { // 优先尝试切换到上次构建队列使用的学期 const termSel = document.querySelector('select[name="term"]'); const savedTerm = localStorage.getItem(LAST_BUILT_KEY); if (termSel && savedTerm && termSel.value !== savedTerm) { termSel.value = savedTerm; termSel.dispatchEvent(new Event('change')); await sleep(800); } q = buildQueueFromListPage(); } if (!q.length) { toast('没有可处理的“待评价”问卷,已完成'); setRunning(false); return; } // 跳转到第一个 const next = q[0]; try { localStorage.setItem(CURRENT_KEY, next); } catch (_) { } location.href = next; } function toast(msg) { const t = document.createElement('div'); t.textContent = msg; t.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);background:#333;color:#fff;padding:8px 12px;border-radius:4px;z-index:999999;opacity:0.95'; document.body.appendChild(t); setTimeout(() => t.remove(), 2500); } function genComments() { const positives = [ '教师课堂组织有序,讲解清晰,重点突出,课堂氛围好。', '教学准备充分,内容充实,案例贴近实际,受益匪浅。', '互动性强,反馈及时,能有效激发学习兴趣与思考。', '课程目标明确,条理清晰,拓展资源丰富,学习收获大。', '联系实际能力强,信息化手段运用恰当,教学效果显著。' ]; const improves = [ '希望适度增加课后练习的讲评比重,便于查漏补缺。', '建议在难点部分延长讲解或提供更多示例与小结。', '可适当增加阶段性学习建议,帮助规划复习节奏。', '建议在答疑时间上更灵活,方便不同同学沟通交流。', '希望提供一些可选拓展阅读,满足不同层次需求。' ]; const pick = (arr) => arr[Math.floor(Math.random() * arr.length)]; return { positive: pick(positives), improve: pick(improves) }; } function markRadio(radio) { if (!radio) return; radio.checked = true; radio.dispatchEvent(new Event('change', { bubbles: true })); radio.click(); } function fillBestForAllRadios() { const groupNames = Array.from(new Set($all('input[type="radio"][name^="answer_"]') .map(r => r.name))) .sort((a, b) => { // 按题号顺序排序,answer_0, answer_1... const ia = parseInt(a.split('_')[1], 10); const ib = parseInt(b.split('_')[1], 10); return ia - ib; }); const selectValue = (name, val) => { const target = $(`input[type="radio"][name="${name}"][value="${val}"]`); if (target) { markRadio(target); return true; } const alt = $all(`input[type="radio"][name="${name}"]`).find(r => r.value !== val); if (alt) { markRadio(alt); return true; } return false; }; // 初始全部选“1”或该组第一个 for (const name of groupNames) { const radios = $all(`input[type="radio"][name="${name}"]`); let best = radios.find(r => r.value === '1'); if (!best) best = radios[0]; markRadio(best); } const ensureRangeNotUniform = (startIdx, endIdx) => { const slice = groupNames.slice(startIdx, endIdx + 1); if (!slice.length) return; const values = slice.map(n => { const checked = $(`input[type="radio"][name="${n}"]:checked`); return checked ? checked.value : null; }); const allSame = values.every(v => v && v === values[0]); if (allSame) { // 将最后一题切换为次优项,避免全部相同 const name = slice[slice.length - 1]; const currentVal = values[0]; const radios = $all(`input[type="radio"][name="${name}"]`); const alt = radios.find(r => r.value !== currentVal) || radios[0]; markRadio(alt); } }; // 1-15题不能完全一样(适用于体验调查,若不足15题则跳过) if (groupNames.length >= 15) { ensureRangeNotUniform(0, 14); } // 4-9题不能完全一样(索引3-8) if (groupNames.length >= 9) { ensureRangeNotUniform(3, 8); } // 再次兜底,保证每组有选中 for (const name of groupNames) { const checked = $(`input[type="radio"][name="${name}"]:checked`); if (!checked) { const first = $(`input[type="radio"][name="${name}"]`); markRadio(first); } } } function fillTextareasSmart() { const areas = $all('textarea[name^="answer_"]'); if (!areas.length) return; const { positive, improve } = genComments(); if (areas.length >= 2) { // 通常第一个问“最满意”,第二个问“最需要改进” if (!areas[0].value) areas[0].value = positive; if (!areas[1].value) areas[1].value = improve; // 其余如有,则统一填充正向+简要建议 for (let i = 2; i < areas.length; i++) { if (!areas[i].value) areas[i].value = positive + ' 同时,' + improve; } } else { if (!areas[0].value) areas[0].value = positive + ' 同时,' + improve; } } async function submitFormWithConfirm() { // 触发页面自带校验 getFormInfo(),然后点击确认弹窗或直接调用 save_submit() const btn = $('#btn_sub'); if (btn) btn.click(); await sleep(300); // 若弹出“返回检查”的校验层,先关闭再重填并重试 const checkMask = document.querySelector('#c'); if (checkMask && getComputedStyle(checkMask).display !== 'none') { const backBtn = checkMask.querySelector('input[type="button"]'); if (backBtn) backBtn.click(); else checkMask.style.display = 'none'; fillBestForAllRadios(); fillTextareasSmart(); if (btn) btn.click(); await sleep(300); } // 优先直接调用 save_submit() try { if (typeof window.save_submit === 'function') { window.save_submit(); } } catch (_) { } // 或者点击弹窗中的“提交”按钮 const tryClickConfirm = () => { const confirmBtn = $all('input[type="button"]').find(b => /提交/.test(b.value || '')); if (confirmBtn) confirmBtn.click(); }; tryClickConfirm(); // 兜底:再次尝试 await sleep(600); tryClickConfirm(); } async function handlePagerPage() { // 等待表单渲染 await sleep(200); fillBestForAllRadios(); fillTextareasSmart(); await sleep(150); await submitFormWithConfirm(); // 若未跳转,3s 后返回列表,继续调度 setTimeout(() => { if (location.search.includes('a=pager')) { location.href = 'index.php?c=index&a=pagerList'; } }, 3000); } async function handleListPage() { createFloatPanel(); if (!isRunning()) return; // 手动点击后才开始 // 尝试确保学期一致,避免因切换学期导致“看不到待评价” const termSel = document.querySelector('select[name="term"]'); const savedTerm = localStorage.getItem(LAST_BUILT_KEY); if (termSel && savedTerm && termSel.value !== savedTerm) { termSel.value = savedTerm; termSel.dispatchEvent(new Event('change')); await sleep(800); } // 每次回到列表页都重新收集一次队列,避免漏判 const queue = buildQueueFromListPage(); if (!queue.length) { toast('没有可处理的“待评价”问卷,已完成'); setRunning(false); return; } await sleep(400); scheduleNextFromList(); } // 入口 (async function main() { try { if (onPagerPage()) { await handlePagerPage(); } else if (onListPage()) { await handleListPage(); } } catch (err) { console.error('[HLJU auto-eval] error:', err); } })(); })();