// ==UserScript== // @name 自动评教by ittel:适用于江苏大学 // @namespace // @version 4.0.0 // @author ITTEL // @description 全自动评教:填充→提交→确认→下一课程→翻页→多教师tab,全程无人值守 // @license MIT // @icon https://ts3.tc.mm.bing.net/th/id/ODF.OBdTb_bnewqEd7HjDCi4mg?w=32&h=32&qlt=90&pcl=fffffa&o=6&pid=1.2 // @match *://*.mycospxk.com/* // @match *://*.edu.cn/* // @require https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js // @run-at document-start // ==/UserScript== (function ($) { 'use strict'; const CFG = { // 权重随机:同意(4) > 非常同意(5) > 一般(3),不选 1 和 2 weights: { 3: 15, 4: 50, 5: 35 }, comment: "我对本课程非常满意。", submitDelay: 600, actionDelay: 1800, formWaitRetries: 20, formWaitInterval: 500 }; const ST = { running: false, stopped: false, total: 0, done: 0, log: [] }; /* ==================== 工具函数 ==================== */ const fillNative = (el, val) => { const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; Object.getOwnPropertyDescriptor(proto, 'value').set.call(el, val); el.dispatchEvent(new Event('input', { bubbles: true })); }; const weightedPick = (obj) => { const entries = Object.entries(obj).map(([k, v]) => [Number(k), Math.max(0, v)]); const total = entries.reduce((s, e) => s + e[1], 0); if (!total) return entries[0][0]; let r = Math.random() * total; for (const [k, v] of entries) { r -= v; if (r <= 0) return k; } return entries[entries.length - 1][0]; }; const sleep = (ms) => new Promise(r => setTimeout(r, ms)); /* ==================== 填充逻辑 ==================== */ const fillRadios = () => { let filled = 0; $('.ant-radio-group').each((_, group) => { const $g = $(group); // 跳过已禁用(已评教) if ($g.find('input:disabled').length === $g.find('input').length) return; const $opts = $g.find('.ant-radio-wrapper'); if (!$opts.length) return; // 按文本精确匹配 → 按 value 数值匹配 const map = {}; $opts.each((_, o) => { const txt = $(o).text().trim(); const v = $(o).find('input').attr('value'); if (v) map[v] = { txt, $el: $(o) }; }); // 选择目标:优先从权重里选一个 value,再点击对应选项 const targetVal = String(weightedPick(CFG.weights)); const target = map[targetVal]; if (target) { // 用原生 click 确保 React/Ant Design 响应 const input = target.$el.find('input[type=radio]')[0]; if (input) input.click(); else target.$el.trigger('click'); filled++; } }); return filled; }; const fillCheckboxes = () => { let filled = 0; $('.ant-checkbox-group').each((_, group) => { $(group).find('.ant-checkbox-input').each((_, input) => { if (!input.checked && !input.disabled) { input.click(); filled++; } }); }); return filled; }; const fillTextarea = () => { let filled = 0; $('textarea.ant-input').each((_, el) => { if (el.disabled || el.readOnly) return; fillNative(el, CFG.comment); filled++; }); return filled; }; /* ==================== 页面操作 ==================== */ const findSubmitBtn = () => { return $('button.ant-btn-primary').filter(function () { const t = $(this).text().replace(/\s+/g, ''); return /提交/.test(t) && !$(this).hasClass('ujs-auto-btn'); }).first(); }; const clickSubmit = () => { const $btn = findSubmitBtn(); if ($btn.length) { $btn[0].click(); return true; } return false; }; const dismissModal = async () => { // 等待确认弹窗出现 for (let i = 0; i < 20; i++) { await sleep(300); const $modal = $('div.ant-modal-body'); if (!$modal.length) continue; const $btn = $modal.find('button.ant-btn-primary'); if ($btn.length) { $btn[0].click(); return true; } const $ok = $modal.find('button').filter(function () { return /确[定认]|OK|ok/.test($(this).text().trim()); }); if ($ok.length) { $ok[0].click(); return true; } } return false; }; const clickNextCourse = async () => { await sleep(CFG.actionDelay); // 优先找"下一课程"按钮 const $next = $('button').filter(function () { return /下一课程/.test($(this).text().trim()); }); if ($next.length && !$next.prop('disabled')) { $next[0].click(); return true; } return false; }; const clickNextPage = () => { const $next = $('.ant-pagination-next:not(.ant-pagination-disabled)'); if ($next.length) { $next[0].click(); return true; } return false; }; const switchToNextTab = () => { const $tabs = $('.ant-tabs-tab'); const $active = $tabs.filter('.ant-tabs-tab-active'); const idx = $tabs.index($active); if (idx < $tabs.length - 1) { $tabs.eq(idx + 1)[0].click(); return true; } return false; }; const waitForForm = async () => { for (let i = 0; i < CFG.formWaitRetries; i++) { await sleep(CFG.formWaitInterval); if ($('.ant-radio-group').length || $('textarea.ant-input').length) return true; } return false; }; /* ==================== 列表页课程点击 ==================== */ const clickFirstUnfinished = () => { const $rows = $('tbody tr.ant-table-row, .ant-table-row'); for (let i = 0; i < $rows.length; i++) { const $row = $rows.eq(i); const text = $row.text(); // 跳过已完成的行 if (text.includes('已评') || text.includes('已完成')) continue; // 找到"评价"链接并点击 const $link = $row.find('td:last-child span, td:last-child a'); if ($link.length) { $link[0].click(); return true; } } return false; }; const countListItems = () => { return $('tbody tr.ant-table-row, .ant-table-row').filter(function () { return $(this).text().includes('进行中') || $(this).text().includes('未完成'); }).length; }; /* ==================== 状态面板 ==================== */ const $panel = (() => { const $p = $(`
全自动评教引擎 v4.0
等待启动...
`); $p.find('.ujs-stop-btn').on('click', () => { ST.stopped = true; }); return $p; })(); const panelApi = { show() { $panel.appendTo('body'); }, hide() { $panel.detach(); }, text(t) { $panel.find('.ujs-panel-text').text(t); }, progress(pct) { $panel.find('.ujs-panel-bar-inner').css('width', pct + '%'); }, log(msg) { ST.log.push(msg); const $log = $panel.find('.ujs-panel-log'); $log.append(`
${msg}
`); $log.scrollTop($log[0].scrollHeight); } }; /* ==================== 核心主循环 ==================== */ const mainLoop = async () => { if (ST.running) return; ST.running = true; ST.stopped = false; ST.done = 0; ST.total = countListItems(); panelApi.show(); panelApi.log('🚀 开始全自动评教...'); const processOneCourse = async () => { if (ST.stopped) return; // 0. 如果在列表页,先点击一门未完成的课程 if (isDetailsPage()) { const clicked = clickFirstUnfinished(); if (!clicked) { panelApi.log('⚠️ 未找到未完成的课程'); return; } panelApi.log('📖 已点击课程,等待表单加载...'); await sleep(CFG.actionDelay); } // 1. 等待表单加载 panelApi.text(`正在评教 (${ST.done}/${ST.total})...`); const hasForm = await waitForForm(); if (!hasForm) { panelApi.log('⚠️ 未检测到评价表单,可能已全部完成'); return; } // 2. 处理所有教师 tab const $tabs = $('.ant-tabs-tab'); const tabCount = $tabs.length || 1; for (let t = 0; t < tabCount; t++) { if (ST.stopped) return; if (t > 0) { // 切换到下一个教师 tab const switched = switchToNextTab(); if (!switched) break; await sleep(1000); await waitForForm(); panelApi.log(` 📋 切换到教师 tab ${t + 1}/${tabCount}`); } // 3. 填充 const rCount = fillRadios(); fillCheckboxes(); fillTextarea(); panelApi.log(` ✓ 已填充: ${rCount} 道单选 + 文本评价`); // 4. 提交 await sleep(CFG.submitDelay); const submitted = clickSubmit(); if (!submitted) { panelApi.log(' ⚠️ 未找到提交按钮'); continue; } panelApi.log(' ⏳ 已点击提交...'); // 5. 确认弹窗 const confirmed = await dismissModal(); if (confirmed) panelApi.log(' ✓ 已确认提交弹窗'); await sleep(800); } // 6. 一门课完成 ST.done++; const pct = ST.total > 0 ? Math.round((ST.done / ST.total) * 100) : 0; panelApi.progress(pct); panelApi.log(`✅ 第 ${ST.done} 门评教完成 (${pct}%)`); }; // 主循环:不断找下一门课 while (!ST.stopped) { await processOneCourse(); if (ST.stopped) break; // 优先:在答题页找"下一课程"按钮 if (isAnswerPage()) { const hasNext = await clickNextCourse(); if (hasNext) { panelApi.log('→ 点击「下一课程」'); await sleep(CFG.actionDelay); // 等待新表单加载 await waitForForm(); continue; } // 没有"下一课程"了,回列表页 panelApi.log('→ 返回列表页...'); history.back(); await sleep(CFG.actionDelay * 2); } // 在列表页:尝试翻页 if (isDetailsPage()) { const remaining = countListItems(); if (remaining > 0) { // 当前页还有未完成的,继续 continue; } const hasNextPage = clickNextPage(); if (hasNextPage) { panelApi.log('→ 翻到下一页'); await sleep(CFG.actionDelay * 2); ST.total = ST.total + countListItems(); continue; } // 没有下一页了 break; } } // 完成 ST.running = false; if (ST.stopped) { panelApi.text('已手动停止'); panelApi.log('⛔ 用户手动停止'); } else { panelApi.text('全部完成 ✓'); panelApi.progress(100); panelApi.log(`🎉 全部评教完成!共 ${ST.done} 门课`); } }; /* ==================== 列表页检测 ==================== */ const isDetailsPage = () => location.hash.includes('/my-task/details/'); const isAnswerPage = () => location.hash.includes('/my-task/answer/'); const addButtons = () => { if ($('#ujs-auto-btn').length) return; if (!isDetailsPage()) return; const $btn = $(``); $btn.on('click', () => { mainLoop(); }); // 插入到表格上方 const $wrapper = $('.ant-table-wrapper'); if ($wrapper.length) { $wrapper.first().before($btn); } else { const $content = $('.ant-layout-content').first() || $('main').first(); if ($content.length) $content.prepend($btn); else $('body').append($btn.css({ position: 'fixed', bottom: '80px', left: '20px', zIndex: 999999 })); } }; /* ==================== 样式注入 ==================== */ const injectStyles = () => { if (document.getElementById('ujs-auto-style')) return; const s = document.createElement('style'); s.id = 'ujs-auto-style'; s.textContent = ` #ujs-auto-btn { margin: 12px 0; border-radius: 8px; font-weight: 700; font-size: 14px; padding: 8px 24px; background: linear-gradient(135deg, #10b981, #14b8a6); border-color: #10b981; color: #fff; box-shadow: 0 4px 14px rgba(16,185,129,0.3); transition: all .2s; } #ujs-auto-btn:hover { transform: translateY(-1px); box-shadow: 0 8px 20px rgba(16,185,129,0.4); } #ujs-panel { position: fixed; bottom: 20px; right: 20px; z-index: 999999; background: rgba(15,23,42,0.95); backdrop-filter: blur(16px); border: 1px solid rgba(139,92,246,0.3); border-radius: 16px; padding: 16px 20px; min-width: 300px; color: #e2e8f0; font-size: 13px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; box-shadow: 0 20px 40px rgba(0,0,0,0.4); } .ujs-panel-title { font-weight: 700; font-size: 15px; margin-bottom: 10px; display: flex; align-items: center; gap: 6px; } @keyframes ujs-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .ujs-spin { display: inline-block; animation: ujs-spin 1.5s linear infinite; } .ujs-panel-bar { height: 5px; background: rgba(255,255,255,0.1); border-radius: 3px; margin: 8px 0; overflow: hidden; } .ujs-panel-bar-inner { height: 100%; background: linear-gradient(90deg, #8b5cf6, #a855f7); border-radius: 3px; transition: width .5s ease; width: 0%; } .ujs-panel-text { color: #cbd5e1; font-size: 12px; margin-bottom: 6px; } .ujs-panel-log { max-height: 140px; overflow-y: auto; font-size: 12px; color: #94a3b8; line-height: 1.7; } .ujs-panel-log::-webkit-scrollbar { width: 4px; } .ujs-panel-log::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 2px; } .ujs-panel-actions { display: flex; gap: 8px; margin-top: 10px; } .ujs-panel-btn { flex: 1; padding: 6px 12px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.15); background: rgba(255,255,255,0.08); color: #e2e8f0; font-size: 12px; font-weight: 600; cursor: pointer; transition: background .2s; } .ujs-panel-btn:hover { background: rgba(255,255,255,0.15); } .ujs-stop-btn { border-color: rgba(239,68,68,0.4); color: #fca5a5; } `; (document.head || document.documentElement).appendChild(s); }; /* ==================== 初始化 ==================== */ const init = () => { injectStyles(); // URL 变化监听(SPA hash 路由) let urlTimer = null; const onUrlChange = () => { clearTimeout(urlTimer); urlTimer = setTimeout(() => { $('#ujs-auto-btn').remove(); addButtons(); }, 600); }; const origPush = history.pushState; history.pushState = function () { origPush.apply(this, arguments); onUrlChange(); }; const origReplace = history.replaceState; history.replaceState = function () { origReplace.apply(this, arguments); onUrlChange(); }; window.addEventListener('popstate', onUrlChange); window.addEventListener('hashchange', onUrlChange); // 立即尝试添加按钮(页面可能已加载完毕) addButtons(); // 轮询兜底:SPA 框架可能延迟渲染 DOM let pollCount = 0; const poll = setInterval(() => { addButtons(); if (++pollCount > 10 || document.getElementById('ujs-auto-btn')) clearInterval(poll); }, 800); // MutationObserver 补充:监听动态 DOM 变更 if (document.body) { const obs = new MutationObserver(() => { addButtons(); }); obs.observe(document.body, { childList: true, subtree: true }); // 30 秒后停止观察,避免性能影响 setTimeout(() => obs.disconnect(), 30000); } }; // 入口:直接执行(userscript @run-at document-start + jQuery @require 保证 jQuery 已加载) init(); })(jQuery);