// ==UserScript== // @name 沈阳师范大学 自动化教务评课脚本 // @namespace http://tampermonkey.net/ // @version 4.1 // @description 融合多个版本优点,全新悬浮窗UI,修复了核心的评教任务识别逻辑,通过分析页面结构,实现了在课程间自动切换和保存。专为沈阳师范大学优化。 // @author DianaChen // @match https://210-30-208-218.webvpn.synu.edu.cn/*/xsjxpj.aspx* // @match https://210-30-208-200.webvpn.synu.edu.cn/*/xsjxpj.aspx* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // --- 1. 配置与常量 (Configuration & Constants) --- const CONFIG = { ITEMS_THRESHOLD: 10, // 当评价项多于此数量时,会选择2个“良好”,否则为1个 RATING_OPTION_EXCELLENT: 1, // 选项“优秀”的索引 (selectedIndex) RATING_OPTION_GOOD: 2, // 选项“良好”的索引 (selectedIndex) }; const SELECTORS = { PAGE: { // 核心修复:评教任务不再是左侧链接,而是页面内的下拉选择框 TASK_DROPDOWN: "#pjkc", // TASK_OPTIONS: "#pjkc > option" // }, FORM: { // 核心修复:选择器更精确,直接定位评教表单内的元素 EVALUATION_SELECTS: '#DataGrid1 select[id*="JS1"]', // SAVE_BUTTON: "#Button1", // SUBMIT_ALL_BUTTON: "#Button2", // COMMENT_BOX: "#pjxx" // }, UI: { CONTAINER: "#eval-container", FAB: "#eval-fab", PANEL: "#eval-panel", HEADER: "#eval-header", CLOSE_BTN: "#eval-close-btn", START_BTN: "#eval-start-btn", RESET_BTN: "#eval-reset-btn", STATUS_LOG: "#eval-status-log", PROGRESS_BAR_FILL: "#eval-progress-bar-fill", STATS_DISPLAY: "#eval-stats", } }; const STORAGE_KEYS = { IS_EVALUATING: "eval_isEvaluating", TASK_QUEUE: "eval_taskQueue", CURRENT_INDEX: "eval_currentIndex", COMPLETED_COUNT: "eval_completedCount" }; // --- 2. 状态管理 (State Management) --- const state = { taskQueue: [], totalTasks: 0, currentIndex: 0, completedTasks: 0, isProcessing: false, }; // --- 3. UI 定义 (UI Definitions) --- const uiHTML = `
🎓
`; // --- 全面优化的CSS样式 --- const uiCSS = ` #eval-container { position: fixed; bottom: 30px; right: 30px; z-index: 9998; } #eval-fab { width: 56px; height: 56px; background-color: #0d6efd; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 28px; cursor: pointer; box-shadow: 0 4px 10px rgba(0,0,0,0.2); transition: all 0.3s ease; user-select: none; } #eval-fab:hover { background-color: #0b5ed7; transform: scale(1.1) rotate(15deg); } #eval-panel { position: absolute; bottom: 70px; right: 0; width: 320px; background-color: #ffffff; border: 1px solid #dee2e6; border-radius: 12px; box-shadow: 0 6px 20px rgba(0,0,0,0.15); font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif; z-index: 9999; color: #333; transform-origin: bottom right; transition: transform 0.3s ease, opacity 0.3s ease; } #eval-panel.hidden { transform: scale(0.5); opacity: 0; pointer-events: none; } #eval-header { padding: 12px 15px; background-color: #0d6efd; color: white; border-top-left-radius: 11px; border-top-right-radius: 11px; cursor: move; user-select: none; display: flex; justify-content: space-between; align-items: center; } #eval-header span { font-weight: bold; } #eval-close-btn { cursor: pointer; font-size: 20px; padding: 0 5px; opacity: 0.8; transition: opacity 0.2s; } #eval-close-btn:hover { opacity: 1; } #eval-body { padding: 15px; } /* 修正:状态日志字体颜色加深,确保可读性 */ #eval-status-log { margin-bottom: 12px; font-size: 14px; min-height: 40px; background-color: #f8f9fa; padding: 10px; border-radius: 6px; border: 1px solid #e9ecef; text-align: center; color: #343a40; /* 使用更深的颜色 */ } .progress-container { width: 100%; background-color: #e9ecef; border-radius: 8px; overflow: hidden; margin-bottom: 8px; height: 16px; } #eval-progress-bar-fill { width: 0%; height: 100%; background-color: #198754; transition: width 0.4s ease-in-out; text-align: center; color: white; line-height: 16px; font-size: 12px; font-weight: bold; } #eval-stats { text-align: right; font-size: 12px; color: #6c757d; margin-bottom: 15px; } .controls { display: grid; grid-template-columns: 2fr 1fr; gap: 10px; align-items: center; } /* 修正:按钮统一样式,并使用flex修正对齐问题 */ .action-btn, .reset-btn { color: #ffffff !important; /* 统一白色字体 */ border: none !important; padding: 10px 15px !important; border-radius: 8px !important; cursor: pointer !important; font-size: 16px !important; font-weight: bold; transition: all 0.2s ease !important; display: flex; /* 关键:使用Flexbox修正内容对齐 */ align-items: center; /* 垂直居中 */ justify-content: center; /* 水平居中 */ gap: 8px; /* 为可能的图标和文本提供间距 */ } /* 修正:更新按钮颜色方案 */ .action-btn { background-color: #198754 !important; } /* 开始按钮:活力绿 */ .action-btn:hover:not(:disabled) { background-color: #157347 !important; box-shadow: 0 2px 8px rgba(0,0,0,0.15); } .action-btn:active:not(:disabled) { transform: scale(0.97); } .reset-btn { background-color: #dc3545 !important; font-size: 14px !important; } /* 停止按钮:警示红 */ .reset-btn:hover:not(:disabled) { background-color: #c82333 !important; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .reset-btn:active:not(:disabled) { transform: scale(0.97); } /* 修正:通过降低不透明度来处理禁用状态,解决字体颜色看不清的问题 */ .action-btn:disabled, .reset-btn:disabled { background-color: #cccccc !important; cursor: not-allowed !important; opacity: 0.7; } `; // --- 4. DOM 元素缓存 (DOM Element Cache) --- const dom = {}; // --- 5. 工具函数 (Utility Functions) --- function updateUI(message, progress, stats) { if (dom.statusLog && message !== null) dom.statusLog.textContent = message; if (dom.progressBarFill && progress !== null) { dom.progressBarFill.style.width = `${progress}%`; dom.progressBarFill.textContent = `${Math.round(progress)}%`; } if (dom.statsDisplay && stats !== null) dom.statsDisplay.textContent = stats; } // --- 6. 核心逻辑函数 (Core Logic Functions) --- function fillEvaluationForm() { const selects = document.querySelectorAll(SELECTORS.FORM.EVALUATION_SELECTS); if (selects.length === 0) { throw new Error("在当前页面未找到任何评教选项(Selects)。"); } selects.forEach(s => s.selectedIndex = CONFIG.RATING_OPTION_EXCELLENT); const numOfGoodRatings = selects.length > CONFIG.ITEMS_THRESHOLD ? 2 : 1; const randomIndexes = new Set(); while (randomIndexes.size < numOfGoodRatings && randomIndexes.size < selects.length) { randomIndexes.add(Math.floor(Math.random() * selects.length)); } randomIndexes.forEach(index => selects[index].selectedIndex = CONFIG.RATING_OPTION_GOOD); const commentBox = document.querySelector(SELECTORS.FORM.COMMENT_BOX); if (commentBox) { const comments = ["老师讲课生动有趣,内容充实,收获很大!", "非常喜欢老师的教学风格。", "老师备课充分,治学严谨,感谢老师的付出。", "这是一门很有价值的课程。"]; commentBox.value = comments[Math.floor(Math.random() * comments.length)]; } } async function processCurrentTask() { const currentCourseValue = state.taskQueue[state.currentIndex]; const dropdown = document.querySelector(SELECTORS.PAGE.TASK_DROPDOWN); if (dropdown.value !== currentCourseValue) { updateUI(`切换到课程: ${dropdown.options[state.currentIndex].text}...`, (state.completedTasks / state.totalTasks) * 100, `进度: ${state.completedTasks} / ${state.totalTasks}`); dropdown.value = currentCourseValue; dropdown.dispatchEvent(new Event('change', { bubbles: true })); return; } try { updateUI(`正在评价: ${dropdown.options[dropdown.selectedIndex].text}`, (state.completedTasks / state.totalTasks) * 100, `进度: ${state.completedTasks} / ${state.totalTasks}`); fillEvaluationForm(); await GM_setValue(STORAGE_KEYS.COMPLETED_COUNT, state.completedTasks + 1); const saveButton = document.querySelector(SELECTORS.FORM.SAVE_BUTTON); if (!saveButton || saveButton.disabled) { throw new Error("“保存”按钮未找到或被禁用。"); } saveButton.click(); } catch (error) { console.error("评价过程中出错:", error); updateUI(`❌ 错误: ${error.message}`, null, null); await resetEvaluation(); } } async function startEvaluation() { if (state.isProcessing) return; const taskOptions = document.querySelectorAll(SELECTORS.PAGE.TASK_OPTIONS); if (!taskOptions || taskOptions.length === 0) { updateUI("❌ 未在本页检测到待评价课程。", 0, "进度: 0 / 0"); return; } dom.startBtn.disabled = true; dom.resetBtn.disabled = false; state.isProcessing = true; const taskQueue = Array.from(taskOptions).map(opt => opt.value); const currentIndex = document.querySelector(SELECTORS.PAGE.TASK_DROPDOWN).selectedIndex; await GM_setValue(STORAGE_KEYS.IS_EVALUATING, true); await GM_setValue(STORAGE_KEYS.TASK_QUEUE, JSON.stringify(taskQueue)); await GM_setValue(STORAGE_KEYS.CURRENT_INDEX, currentIndex); await GM_setValue(STORAGE_KEYS.COMPLETED_COUNT, 0); await loadStateAndContinue(); } async function resetEvaluation() { await GM_deleteValue(STORAGE_KEYS.IS_EVALUATING); await GM_deleteValue(STORAGE_KEYS.TASK_QUEUE); await GM_deleteValue(STORAGE_KEYS.CURRENT_INDEX); await GM_deleteValue(STORAGE_KEYS.COMPLETED_COUNT); state.isProcessing = false; updateUI("评教已停止/重置。", 0, "进度: 0 / 0"); dom.startBtn.disabled = false; dom.startBtn.textContent = "🚀 开始评教"; dom.resetBtn.disabled = true; } async function loadStateAndContinue() { state.isProcessing = await GM_getValue(STORAGE_KEYS.IS_EVALUATING, false); if (!state.isProcessing) { updateUI("等待指令...", 0, "进度: 0 / 0"); dom.resetBtn.disabled = true; return; } const storedQueue = await GM_getValue(STORAGE_KEYS.TASK_QUEUE, "[]"); state.taskQueue = JSON.parse(storedQueue); state.totalTasks = state.taskQueue.length; state.currentIndex = document.querySelector(SELECTORS.PAGE.TASK_DROPDOWN).selectedIndex; state.completedTasks = await GM_getValue(STORAGE_KEYS.COMPLETED_COUNT, 0); dom.startBtn.disabled = true; dom.resetBtn.disabled = false; if (state.completedTasks >= state.totalTasks) { updateUI("🎉 全部评价完成!请手动点击“提交”按钮。", 100, `进度: ${state.totalTasks} / ${state.totalTasks}`); const submitBtn = document.querySelector(SELECTORS.FORM.SUBMIT_ALL_BUTTON); if (submitBtn) { submitBtn.style.cssText = 'border: 3px solid red !important; transform: scale(1.1); background-color: #dc3545 !important;'; submitBtn.disabled = false; } await resetEvaluation(); dom.startBtn.textContent = "✅ 已完成"; } else { await processCurrentTask(); } } // --- 7. UI 设置与事件监听 (UI Setup & Event Listeners) --- function addEventListeners() { dom.fab.onclick = () => dom.panel.classList.toggle('hidden'); dom.closeBtn.onclick = () => dom.panel.classList.add('hidden'); dom.startBtn.onclick = startEvaluation; dom.resetBtn.onclick = resetEvaluation; let isDragging = false, offset = { x: 0, y: 0 }; dom.header.onmousedown = (e) => { isDragging = true; const panelRect = dom.panel.getBoundingClientRect(); offset = { x: e.clientX - panelRect.left, y: e.clientY - panelRect.top }; document.body.style.userSelect = 'none'; }; document.onmousemove = (e) => { if (!isDragging) return; const newX = e.clientX - offset.x; const newY = e.clientY - offset.y; dom.panel.style.position = 'fixed'; dom.panel.style.left = `${newX}px`; dom.panel.style.top = `${newY}px`; dom.panel.style.bottom = 'auto'; dom.panel.style.right = 'auto'; }; document.onmouseup = () => { isDragging = false; document.body.style.userSelect = 'auto'; }; } function setupUI() { GM_addStyle(uiCSS); document.body.insertAdjacentHTML('beforeend', uiHTML); Object.keys(SELECTORS.UI).forEach(key => { dom[key.toLowerCase().replace(/_([a-z])/g, g => g[1].toUpperCase())] = document.querySelector(SELECTORS.UI[key]); }); } // --- 8. 初始化 (Initialization) --- function init() { if (document.querySelector(SELECTORS.UI.CONTAINER)) return; try { setupUI(); addEventListeners(); loadStateAndContinue(); } catch (error) { console.error('自动评教助手初始化失败:', error); alert('自动评教助手初始化失败,请按 F12 查看控制台获取更多信息。'); } } init(); })();