// ==UserScript== // @name 教学评教全自动批量(武昌工学院适配) // @namespace http://learn.demo/ // @version 3.6-newname // @description 仅用于WUIT教评系统,修复初始页面点击跳过第一页的问题 // @author kk // @match *://jxpj.wuit.cn/* // @run-at document-start // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; /* ==================== 配置区 ==================== */ const CONFIG = { SILENT_MODE: true, SCORE_PLANS: [ [10,10,10,10,10,10,10,10,9,9], [10,10,10,10,10,9,9,9,9,9] ], DELAY: { DIALOG_RENDER: 500, INPUT_RETRY: 300, BEFORE_SUBMIT: 400, AFTER_SUBMIT: 1000, BETWEEN_ROWS: 600, PAGE_TURN: 1200, PAGE_JUMP_WAIT: 1500, // 新增:跳转后额外等待页面稳定 }, TIMEOUT: { DIALOG_FILL: 2500, DIALOG_SHOW: 4500, DIALOG_HIDE: 3000, TABLE_READY: 8000, // 增加:初始跳转时表格可能加载更慢 JUMP_BUTTON: 8000 // 新增:等待跳转按钮超时 }, MAX_RETRY: 3 }; /* ==================== 状态区 ==================== */ let isRunning = false; let shouldStop = false; let totalProgress = { current: 0, total: 0 }; /* ==================== 工具函数 ==================== */ const sleep = ms => new Promise(r => setTimeout(r, ms)); async function waitTableReady() { const start = Date.now(); while (Date.now() - start < CONFIG.TIMEOUT.TABLE_READY) { const rows = document.querySelectorAll('.el-table__body .el-table__row'); // 额外检查:确保表格里确实有"去评价"按钮,防止在初始页面误判 if (rows.length > 0) { for (const row of rows) { const btns = Array.from(row.querySelectorAll('button')); if (btns.some(b => b.textContent.trim() === '去评价')) { return; // 确认是评价页面,且有待评价课程 } } // 如果表格存在但没有任何"去评价"按钮,可能是已评价完或页面不对 // 继续等待,可能页面还在加载 } await sleep(200); } } async function waitDialogShow() { const start = Date.now(); while (Date.now() - start < CONFIG.TIMEOUT.DIALOG_SHOW) { const dialog = document.querySelector('.evaluate-dialog'); if (dialog && dialog.style.display !== 'none') return dialog; await sleep(100); } return null; } async function waitDialogHide() { const start = Date.now(); while (Date.now() - start < CONFIG.TIMEOUT.DIALOG_HIDE) { const dialog = document.querySelector('.evaluate-dialog'); if (!dialog || dialog.style.display === 'none') return true; await sleep(100); } return false; } function generateScores() { const plan = CONFIG.SCORE_PLANS[Math.floor(Math.random() * CONFIG.SCORE_PLANS.length)]; return [...plan].sort(() => Math.random() - 0.5); } function log(msg) { const panel = document.querySelector('#eval-log-content'); if (panel) { const line = document.createElement('div'); line.style.cssText = 'margin-bottom:2px;border-bottom:1px solid #eee;padding-bottom:2px;'; line.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`; panel.prepend(line); while (panel.children.length > 50) panel.lastChild.remove(); } console.log(`[评教脚本] ${msg}`); } /* ==================== 静默模式视觉控制 ==================== */ function setSilentStyle(enable) { if (!enable) { const old = document.querySelector('#silent-style'); if (old) old.remove(); document.body.style.overflow = ''; return; } if (document.querySelector('#silent-style')) return; const style = document.createElement('style'); style.id = 'silent-style'; style.textContent = ` .evaluate-dialog, .v-modal, .el-dialog__wrapper { opacity: 0 !important; pointer-events: auto !important; } body { overflow: auto !important; } #silent-table-cover { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255,255,255,0.01); z-index: 99998; pointer-events: none; } `; document.head.appendChild(style); } function showTableCover(show) { let cover = document.querySelector('#silent-table-cover'); if (show) { if (!cover) { cover = document.createElement('div'); cover.id = 'silent-table-cover'; document.body.appendChild(cover); } } else if (cover) { cover.remove(); } } function updateProgressDisplay() { let badge = document.querySelector('#silent-progress'); if (!badge) { badge = document.createElement('div'); badge.id = 'silent-progress'; badge.style.cssText = ` position:fixed;bottom:20px;right:20px;z-index:999999; background:#67C23A;color:#fff;padding:8px 14px; border-radius:20px;font-size:13px;font-weight:bold; box-shadow:0 2px 8px rgba(0,0,0,0.2);display:none; `; document.body.appendChild(badge); } if (isRunning && totalProgress.total > 0) { badge.style.display = 'block'; badge.textContent = `评教进度 ${totalProgress.current}/${totalProgress.total}`; } else { badge.style.display = 'none'; } } /* ==================== 页面跳转与就绪检测(核心修复) ==================== */ async function autoJumpToEvaluatePage() { if (!location.href.includes('/evaluateTeach/studentEvaluate')) return false; log('📍 检测到初始列表页,寻找跳转入口...'); const start = Date.now(); while (Date.now() - start < CONFIG.TIMEOUT.JUMP_BUTTON) { // 优先找全局的"去评价"入口按钮(通常不在表格行里,而在页面头部或卡片上) const allBtns = Array.from(document.querySelectorAll('button, a, .el-button')); const jumpBtn = allBtns.find(el => { const text = el.textContent.trim(); return (text === '去评价' || text === '进入评价' || text === '开始评价') && !el.closest('.el-table__row'); // 排除表格行内的按钮(那些是单条课程的) }); if (jumpBtn) { log('🚀 自动点击跳转...'); jumpBtn.click(); // 等待 URL 变化,确认真的跳转了 await sleep(CONFIG.DELAY.PAGE_JUMP_WAIT); if (location.href.includes('evaluateTeachStudentCourse')) { log('✅ 跳转完成,等待评价页面渲染...'); return true; } } await sleep(300); } log('⚠️ 未找到全局跳转按钮,请手动点击"去评价"'); return false; } // 核心修复:执行任何操作前,确保已经在评价页面且表格就绪 async function ensureEvaluatePageReady() { // 情况1:还在初始列表页 if (location.href.includes('/evaluateTeach/studentEvaluate')) { const jumped = await autoJumpToEvaluatePage(); if (!jumped) return false; } // 情况2:已经在评价页,但可能还在加载中 if (location.href.includes('evaluateTeachStudentCourse')) { log('⏳ 等待评价页面表格就绪...'); await waitTableReady(); log('✅ 页面就绪'); return true; } // 情况3:在未知页面 log('❌ 当前不在预期的评教页面,请刷新后重试'); return false; } /* ==================== 核心评价逻辑 ==================== */ async function fillDialog(dialogBox) { const start = Date.now(); while (Date.now() - start < CONFIG.TIMEOUT.DIALOG_FILL) { const inputs = Array.from(dialogBox.querySelectorAll('.el-input__inner')) .filter(input => input.closest('td')?.textContent.includes('(0/10)')); if (inputs.length === 10) { const scores = generateScores(); inputs.forEach((input, i) => { input.value = scores[i]; input.dispatchEvent(new Event('input', { bubbles: true })); }); await sleep(CONFIG.DELAY.BEFORE_SUBMIT); const allBtns = Array.from(dialogBox.querySelectorAll('.el-dialog__footer button')); const submitBtn = allBtns.find(b => { const t = b.textContent.trim(); return t === '提交' || t === '确定' || t === '保存'; }) || dialogBox.querySelector('.el-dialog__footer .el-button--primary'); if (submitBtn) submitBtn.click(); return true; } await sleep(CONFIG.DELAY.INPUT_RETRY); } return false; } function getPendingCourses() { const rows = document.querySelectorAll('.el-table__body .el-table__row'); const list = []; rows.forEach(row => { const btns = Array.from(row.querySelectorAll('button')); if (!btns.some(b => b.textContent.trim() === '去评价')) return; const cells = row.querySelectorAll('td'); const course = cells[1]?.textContent?.trim() || ''; const teacher = cells[2]?.textContent?.trim() || ''; const teacherId = cells[3]?.textContent?.trim() || ''; list.push({ key: `${course}::${teacher}`, course, teacher, teacherId }); }); return list; } function findOpenBtnByKey(key) { const rows = document.querySelectorAll('.el-table__body .el-table__row'); for (const row of rows) { const cells = row.querySelectorAll('td'); const c = cells[1]?.textContent?.trim() || ''; const t = cells[2]?.textContent?.trim() || ''; if (`${c}::${t}` === key) { const btn = Array.from(row.querySelectorAll('button')) .find(b => b.textContent.trim() === '去评价'); if (btn) return btn; } } return null; } async function handleSingle(courseInfo) { for (let i = 0; i < CONFIG.MAX_RETRY; i++) { if (shouldStop) return false; const openBtn = findOpenBtnByKey(courseInfo.key); if (!openBtn) { log(`⚠️ 找不到【${courseInfo.course}】,可能已评价`); return false; } openBtn.click(); const dialog = await waitDialogShow(); if (!dialog) { log(`⏳ 等待弹窗超时,重试 (${i + 1}/${CONFIG.MAX_RETRY})`); await sleep(800); continue; } const ok = await fillDialog(dialog); if (!ok) { log(`❌ 填充失败,重试 (${i + 1}/${CONFIG.MAX_RETRY})`); await waitDialogHide(); await sleep(500); continue; } await sleep(CONFIG.DELAY.AFTER_SUBMIT); await waitDialogHide(); return true; } return false; } async function runSinglePage() { await waitTableReady(); const courses = getPendingCourses(); if (courses.length === 0) { log('📄 本页没有待评价课程'); return 0; } log(`📄 本页共 ${courses.length} 条待评价`); let success = 0; totalProgress.total += courses.length; for (const info of courses) { if (shouldStop) break; log(`▶️ [${success + 1}/${courses.length}] ${info.course}(${info.teacher})`); const ok = await handleSingle(info); if (ok) { success++; totalProgress.current++; updateProgressDisplay(); log(`✅ 完成`); } else { log(`🚫 失败(跳过)`); } await sleep(CONFIG.DELAY.BETWEEN_ROWS); } return success; } // 修复:全自动翻页入口增加页面就绪检测 async function runAllPages() { if (isRunning) return; // 核心修复:先确保页面正确且就绪 const ready = await ensureEvaluatePageReady(); if (!ready) { log('❌ 页面准备失败,请手动进入评价页面后重试'); return; } isRunning = true; shouldStop = false; totalProgress = { current: 0, total: 0 }; updateUIState(); if (CONFIG.SILENT_MODE) setSilentStyle(true); let total = 0; let page = 1; while (true) { if (shouldStop) { log('🛑 用户已停止'); break; } log(`========== 第 ${page} 页 ==========`); showTableCover(true); const count = await runSinglePage(); showTableCover(false); total += count; const nextBtn = document.querySelector('.btn-next'); if (!nextBtn || nextBtn.disabled) { log('📌 已到最后一页'); break; } nextBtn.click(); await sleep(CONFIG.DELAY.PAGE_TURN); await waitTableReady(); page++; } log(`🏁 全部结束!成功评价 ${total} 条`); isRunning = false; if (CONFIG.SILENT_MODE) setSilentStyle(false); updateProgressDisplay(); updateUIState(); } async function runManual() { // 单条模式也增加页面检测 if (location.href.includes('/evaluateTeach/studentEvaluate')) { log('📍 当前在列表页,请先进入评价页面'); const ready = await ensureEvaluatePageReady(); if (!ready) return; } const dialog = document.querySelector('.evaluate-dialog'); if (!dialog || dialog.style.display === 'none') { log('⚠️ 请先点击【去评价】打开弹窗!'); return; } const ok = await fillDialog(dialog); log(ok ? '✅ 单条填充并提交成功' : '❌ 单条填充失败'); } /* ==================== 拖拽工具函数 ==================== */ function makeDraggable(element, handle) { let isDragging = false; let startX, startY, startLeft, startTop; handle.style.cursor = 'move'; handle.addEventListener('mousedown', (e) => { if (e.target.closest('#eval-close')) return; isDragging = true; startX = e.clientX; startY = e.clientY; const rect = element.getBoundingClientRect(); element.style.transform = 'none'; element.style.left = rect.left + 'px'; element.style.top = rect.top + 'px'; startLeft = rect.left; startTop = rect.top; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; element.style.left = (startLeft + dx) + 'px'; element.style.top = (startTop + dy) + 'px'; }); document.addEventListener('mouseup', () => { isDragging = false; }); } /* ==================== UI 面板 ==================== */ function createUI() { if (document.querySelector('#eval-helper')) return; const wrap = document.createElement('div'); wrap.id = 'eval-helper'; wrap.style.cssText = ` position:fixed;top:16px;left:50%;transform:translateX(-50%); z-index:999999;background:#fff;border:1px solid #dcdfe6; border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,0.15); padding:12px 16px;font-size:14px;min-width:320px; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; user-select:none; `; const header = document.createElement('div'); header.style.cssText = 'font-weight:bold;margin-bottom:10px;color:#303133;display:flex;justify-content:space-between;align-items:center;'; header.innerHTML = `🛠️ 评教助手 v3.5✕`; wrap.appendChild(header); makeDraggable(wrap, header); const btnRow = document.createElement('div'); btnRow.style.cssText = 'display:flex;gap:8px;margin-bottom:10px;'; const manualBtn = document.createElement('button'); manualBtn.textContent = '单条填充'; manualBtn.style.cssText = 'flex:1;padding:8px 0;background:#F56C6C;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;'; const autoBtn = document.createElement('button'); autoBtn.id = 'eval-auto-btn'; autoBtn.textContent = '全自动翻页'; autoBtn.style.cssText = 'flex:1;padding:8px 0;background:#67C23A;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;'; const stopBtn = document.createElement('button'); stopBtn.id = 'eval-stop-btn'; stopBtn.textContent = '停止'; stopBtn.style.cssText = 'flex:1;padding:8px 0;background:#E6A23C;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;display:none;'; btnRow.append(manualBtn, autoBtn, stopBtn); wrap.appendChild(btnRow); const logHeader = document.createElement('div'); logHeader.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;'; logHeader.innerHTML = '运行日志'; const clearBtn = document.createElement('span'); clearBtn.textContent = '清空'; clearBtn.style.cssText = 'font-size:12px;color:#409EFF;cursor:pointer;'; clearBtn.onclick = () => { const content = document.querySelector('#eval-log-content'); if (content) content.innerHTML = ''; }; logHeader.appendChild(clearBtn); wrap.appendChild(logHeader); const logPanel = document.createElement('div'); logPanel.style.cssText = 'max-height:160px;overflow-y:auto;background:#f5f7fa;padding:8px;border-radius:4px;font-size:12px;color:#606266;'; logPanel.innerHTML = '
'; wrap.appendChild(logPanel); document.body.appendChild(wrap); manualBtn.onclick = runManual; autoBtn.onclick = runAllPages; stopBtn.onclick = () => { shouldStop = true; log('⏸️ 收到停止指令...'); }; document.querySelector('#eval-close').onclick = () => wrap.remove(); log('脚本已加载'); } function updateUIState() { const autoBtn = document.querySelector('#eval-auto-btn'); const stopBtn = document.querySelector('#eval-stop-btn'); if (!autoBtn || !stopBtn) return; if (isRunning) { autoBtn.style.display = 'none'; stopBtn.style.display = 'block'; } else { autoBtn.style.display = 'block'; stopBtn.style.display = 'none'; } } /* ==================== 初始化 ==================== */ async function init() { createUI(); // 初始化时如果已经在初始页,静默预跳转(可选,也可去掉让用户自己点按钮) if (location.href.includes('/evaluateTeach/studentEvaluate')) { log('📍 当前在列表页,请点击"全自动翻页"自动跳转并执行'); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } const observer = new MutationObserver(() => { if (!document.querySelector('#eval-helper')) createUI(); }); observer.observe(document.body, { childList: true, subtree: true }); })();