// ==UserScript== // @name 泉纺强智自动评教 // @namespace https://scriptcat.org/ // @version 1.0 // @description 这是一个专为泉纺的强智教务系统设计的自动评教脚本,帮助同学们快速完成学生评教。 // @author 小哲(Gemini-3-pro辅助开发) // @tag 自动评教 // @match http://192.168.100.7/* // @match http://192.168.100.7/jsxsd/* // @match http://36.249.51.8:8082/* // @icon https://img.icons8.com/fluency/64/maintenance.png // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @run-at document-end // ==/UserScript== (function() { 'use strict'; // --- 0. 样式定义 --- GM_addStyle(` @keyframes qzt-pulse { 0% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); transform: scale(1); } 70% { box-shadow: 0 0 0 15px rgba(59, 130, 246, 0); transform: scale(1.05); } 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); transform: scale(1); } } .qzt-highlight-row { background-color: #eef2ff !important; transition: background-color 0.3s; border-left: 4px solid #3b82f6 !important; } .qzt-submit-glow { animation: qzt-pulse 2s infinite !important; outline: 3px solid #3b82f6 !important; outline-offset: 2px; font-weight: bold !important; z-index: 1000 !important; position: relative; } .qzt-dragging { cursor: move !important; user-select: none !important; opacity: 0.9; transform: scale(1.02); box-shadow: 0 15px 30px rgba(0,0,0,0.2) !important; } .qzt-btn-active { transform: scale(0.95) !important; } `); // --- 配置常量 --- const KEY_AGREE = 'qzt_agree_v18'; const KEY_POS = 'qzt_pos_v18'; const DISCLAIMER_TEXT = `【免责声明】\n\n1. 本脚本仅供学习和研究使用,使用者应遵守相关法律法规。\n2. 因使用本脚本造成的任何问题,开发者不承担责任。\n3. 此脚本仅适配【泉纺】强智教务系统,其他学校可能会无法使用。\n\n点击“确定”后,开启使用脚本。`; // --- 📝 组合式评语库 --- const commentParts = { A: ["老师授课认真,", "教学内容丰富,", "课程设计合理,", "老师治学严谨,", "授课方式新颖,", "备课非常充分,", "老师和蔼可亲,"], B: ["重点突出,条理清晰,", "课堂气氛活跃,", "理论联系实际,", "讲解深入浅出,", "善于引导学生思考,", "案例详实生动,", "逻辑性很强,"], C: ["我们收获很大。", "老师很有耐心。", "是一门很好的课。", "教学效果优良。", "希望能继续保持。", "对学生非常负责。", "同学们都很喜欢。"] }; function generateComment() { const getRand = (arr) => arr[Math.floor(Math.random() * arr.length)]; return getRand(commentParts.A) + getRand(commentParts.B) + getRand(commentParts.C); } // --- 💾 安全存储封装 (已修复) --- const SafeStorage = { get: (key, def) => { try { let val = GM_getValue(key, def); // 修复点1: 增加对 val 的类型检查,并完善 try-catch try { if (val && typeof val === 'string' && (val.trim().startsWith('{') || val.trim().startsWith('['))) { return JSON.parse(val); } } catch (e) { // 仅记录日志,不阻断流程。如果解析失败,说明它可能就是个普通字符串,返回原值即可。 console.debug("QZT: 存储值非JSON格式,按原始字符串处理", e); } return val; } catch (e) { console.warn("QZT: GM_getValue 读取失败,使用默认值", e); return def; } }, set: (key, val) => { try { const valToStore = (typeof val === 'object' && val !== null) ? JSON.stringify(val) : val; GM_setValue(key, valToStore); } catch (e) { console.error("QZT: GM_setValue 写入失败", e); } } }; // --- UI工具:气泡提示 --- function showToast(message, duration = 4000) { const oldToast = document.getElementById('qzt-toast'); if (oldToast) oldToast.remove(); const toast = document.createElement('div'); toast.id = 'qzt-toast'; toast.innerHTML = message; Object.assign(toast.style, { position: 'fixed', top: '20px', left: '50%', transform: 'translateX(-50%)', backgroundColor: 'rgba(30, 41, 59, 0.95)', color: '#fff', padding: '10px 24px', borderRadius: '8px', zIndex: '2147483647', fontSize: '14px', boxShadow: '0 10px 25px rgba(0,0,0,0.2)', opacity: '0', transition: 'opacity 0.3s ease', textAlign: 'center', minWidth: '200px', backdropFilter: 'blur(4px)', border: '1px solid rgba(255,255,255,0.1)' }); document.body.appendChild(toast); requestAnimationFrame(() => { toast.style.opacity = '1'; }); setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 300); }, duration); } // --- 评分工具 --- function getScore(radio) { let score = 0; const val = parseFloat(radio.value); if (!isNaN(val) && val < 100) score = val; const parentText = radio.parentElement ? radio.parentElement.innerText : ""; const match = parentText.match(/[((](\d+(?:\.\d+)?)(?:分)?[))]/); if (match) score = parseFloat(match[1]); return score; } function clearAllHighlights() { document.querySelectorAll('.qzt-highlight-row').forEach(el => { el.classList.remove('qzt-highlight-row'); }); } function highlightRow(element) { let parent = element.parentElement; while(parent && parent.tagName !== 'TR') { parent = parent.parentElement; } if(parent) parent.classList.add('qzt-highlight-row'); } // --- 聚焦提交按钮 (已修复) --- function focusSubmitButton() { let submitBtn = document.querySelector('#btn_xspj_bc'); if (!submitBtn) { const candidates = document.querySelectorAll('input[type="button"], button, a'); for (let btn of candidates) { const txt = (btn.value || btn.innerText || "").trim(); // 排除 "返回" 按钮,优先匹配 "提交" 或 "保存" if ((txt.includes('提交') || txt === '保存') && !txt.includes('返回')) { submitBtn = btn; break; } } } if (submitBtn) { submitBtn.classList.add('qzt-submit-glow'); // 修复点2: 增加 try-catch 并在日志中记录,避免空 catch 屏蔽错误 try { // 兼容性处理:部分浏览器不支持 smooth submitBtn.scrollIntoView({ behavior: 'auto', block: 'center' }); submitBtn.focus({ preventScroll: true }); // 防止 focus 再次触发滚动跳跃 } catch(e) { console.warn("QZT: 自动聚焦按钮失败 (非致命错误)", e); } // 增强: 临时绑定回车键提交 (仅一次) const enterHandler = (e) => { if (e.key === 'Enter') { submitBtn.click(); document.removeEventListener('keydown', enterHandler); } }; document.addEventListener('keydown', enterHandler, { once: true }); return true; } return false; } // --- 核心逻辑 --- function runEvaluation() { if (!SafeStorage.get(KEY_AGREE, false)) { if (!confirm(DISCLAIMER_TEXT)) return; SafeStorage.set(KEY_AGREE, true); showToast("✅ 设置完成,存储功能已修复!"); } clearAllHighlights(); const radios = document.querySelectorAll('input[type="radio"]'); if (radios.length === 0) { showToast("⚠️ 未找到评价项"); return; } const groups = {}; radios.forEach(radio => { if (!groups[radio.name]) groups[radio.name] = []; groups[radio.name].push(radio); }); const groupNames = Object.keys(groups); let count = 0; let totalScore = 0; let deductedCount = 0; // 随机扣分策略 let randomTargetIndex = -1; if (groupNames.length > 2) { randomTargetIndex = Math.floor(Math.random() * groupNames.length); } groupNames.forEach((name, index) => { const radioList = groups[name]; let targetRadio; // 简单逻辑:绝大多数选第一个(通常是最高分),偶尔扣分 if (index === randomTargetIndex && radioList.length > 1) { targetRadio = radioList[1]; deductedCount++; highlightRow(targetRadio); } else { targetRadio = radioList[0]; } if (targetRadio) { targetRadio.click(); targetRadio.checked = true; // 双重保险 count++; totalScore += getScore(targetRadio); } }); // 下拉框处理 document.querySelectorAll('select').forEach(select => { if(select.options.length > 1) { select.selectedIndex = 1; select.dispatchEvent(new Event('change', { bubbles: true })); } }); // 文本框处理 document.querySelectorAll('textarea').forEach(textarea => { if (!textarea.value.trim()) { textarea.value = generateComment(); textarea.dispatchEvent(new Event('input', { bubbles: true })); textarea.dispatchEvent(new Event('change', { bubbles: true })); } }); if (count > 0) { const scoreHtml = totalScore > 0 ? `
预计选项总分:${totalScore}` : ""; const detailHtml = deductedCount > 0 ? `(${deductedCount}项智能扣分)` : ""; showToast(`✅ 评教完成 (${count}项) ${detailHtml}${scoreHtml}\n⌨️ 按回车(Enter)即可提交`, 4000); setTimeout(() => { if (!focusSubmitButton()) { window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); } }, 300); } } // --- 拖拽与位置记忆 (已修复) --- function makeDraggable(element) { let isDragging = false; let startX, startY, initialLeft, initialTop; element.addEventListener('mousedown', (e) => { if (e.button !== 0) return; isDragging = false; startX = e.clientX; startY = e.clientY; const rect = element.getBoundingClientRect(); initialLeft = rect.left; initialTop = rect.top; // 清除定位属性,改为绝对定位计算 element.style.bottom = 'auto'; element.style.right = 'auto'; element.style.left = `${initialLeft}px`; element.style.top = `${initialTop}px`; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); function onMouseMove(e) { const dx = e.clientX - startX; const dy = e.clientY - startY; // 防抖动阈值 if (Math.abs(dx) > 3 || Math.abs(dy) > 3) { isDragging = true; element.classList.add('qzt-dragging'); // 增加边界检查 (优化点) let newLeft = initialLeft + dx; let newTop = initialTop + dy; const maxLeft = window.innerWidth - element.offsetWidth; const maxTop = window.innerHeight - element.offsetHeight; // 限制在屏幕内 newLeft = Math.max(0, Math.min(newLeft, maxLeft)); newTop = Math.max(0, Math.min(newTop, maxTop)); element.style.left = `${newLeft}px`; element.style.top = `${newTop}px`; } } // 修复点3: 修复 onMouseUp 报错与逻辑 function onMouseUp(e) { // 关键修复:无论后续逻辑如何,必须先移除监听器,防止死循环或报错 document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); try { element.classList.remove('qzt-dragging'); if (isDragging) { // 保存位置 SafeStorage.set(KEY_POS, { left: element.style.left, top: element.style.top }); } else { // 点击事件处理 element.classList.add('qzt-btn-active'); setTimeout(() => element.classList.remove('qzt-btn-active'), 150); setTimeout(runEvaluation, 50); } } catch (err) { console.error("QZT: onMouseUp 逻辑异常", err); } } } // --- 创建按钮 --- function createButton() { if (document.getElementById('qzt-eval-btn-v18')) return; const btn = document.createElement('div'); btn.id = 'qzt-eval-btn-v18'; btn.innerHTML = '🔧 一键评教'; btn.title = "点击评教 | 拖拽调整位置 (已开启安全存储)"; let savedPos = SafeStorage.get(KEY_POS, null); Object.assign(btn.style, { position: 'fixed', zIndex: '2147483647', padding: '10px 20px', background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)', color: 'white', borderRadius: '6px', cursor: 'pointer', boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', fontWeight: '600', fontSize: '14px', transition: 'transform 0.1s, box-shadow 0.3s', display: 'flex', alignItems: 'center', gap: '8px', userSelect: 'none', width: 'fit-content', fontFamily: 'system-ui, -apple-system, sans-serif' }); // 位置初始化检查 (优化点) if (savedPos && typeof savedPos === 'object' && savedPos.left && savedPos.left !== "NaNpx") { btn.style.left = savedPos.left; btn.style.top = savedPos.top; } else { btn.style.top = '85%'; btn.style.left = '85%'; } btn.onmouseover = () => { btn.style.transform = 'translateY(-1px)'; btn.style.boxShadow = '0 10px 15px -3px rgba(0, 0, 0, 0.1)'; }; btn.onmouseout = () => { btn.style.transform = 'translateY(0)'; btn.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1)'; }; makeDraggable(btn); document.body.appendChild(btn); } // --- 启动 --- function checkAndInit() { // 只有当页面存在 radio 评价项时才显示按钮 if (document.querySelectorAll('input[type="radio"]').length > 0) { createButton(); } } // 使用 setInterval 兼容动态加载的页面 setInterval(checkAndInit, 1000); })();