// ==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);
})();