// ==UserScript==
// @name 齐大教务助手
// @namespace https://greasyfork.org/users/737539
// @version 2.7.2
// @author 忘忧
// @description 集成抢课功能与教学评估功能,一体化教务助手
// @license MIT
// @icon https://xyh.qqhru.edu.cn/favicon.ico
// @match http://111.43.36.164/*
// @match http://172.20.139.153:7700/*
// @match https://172-20-139-153-7700.webvpn.qqhru.edu.cn/*
// @connect scriptcat.org
// @grant GM_deleteValue
// @grant GM_getValue
// @grant GM_notification
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// ==/UserScript==
(function () {
'use strict';
const EducationHelper = {};
EducationHelper.Storage = {
prefix: "qqhru-helper:",
supportsGM: function() {
return typeof GM_getValue === "function" && typeof GM_setValue === "function";
},
getStorageKey: function(key) {
return `${this.prefix}${key}`;
},
get: function(key, fallback = null) {
const storageKey = this.getStorageKey(key);
try {
if (this.supportsGM()) {
const rawValue = GM_getValue(storageKey, null);
if (rawValue !== null && rawValue !== void 0) {
return JSON.parse(rawValue);
}
}
} catch (error) {
console.warn("[警告] 读取 GM 存储失败:", error);
}
try {
const rawValue = localStorage.getItem(storageKey);
if (rawValue !== null) {
return JSON.parse(rawValue);
}
} catch (error) {
console.warn("[警告] 读取 localStorage 失败:", error);
}
return fallback;
},
set: function(key, value) {
const storageKey = this.getStorageKey(key);
const serializedValue = JSON.stringify(value);
if (this.supportsGM()) {
GM_setValue(storageKey, serializedValue);
}
localStorage.setItem(storageKey, serializedValue);
},
remove: function(key) {
const storageKey = this.getStorageKey(key);
try {
if (typeof GM_deleteValue === "function") {
GM_deleteValue(storageKey);
}
} catch (error) {
console.warn("[警告] 删除 GM 存储失败:", error);
}
localStorage.removeItem(storageKey);
}
};
EducationHelper.Logger = {
// 调试日志
debug: function(message, data) {
if (EducationHelper.Config.state.debugMode) {
console.log(`[调试] ${message}`, data || "");
const debugContent = document.getElementById("debugContent");
if (debugContent) {
const logItem = document.createElement("div");
logItem.style.borderBottom = "1px dashed #eee";
logItem.style.paddingBottom = "3px";
logItem.style.marginBottom = "3px";
let logText = message;
if (data) {
if (typeof data === "object") {
try {
logText += ` ${JSON.stringify(data)}`;
} catch (e) {
logText += ` [复杂对象]`;
}
} else {
logText += ` ${data}`;
}
}
logItem.textContent = logText;
debugContent.appendChild(logItem);
debugContent.scrollTop = debugContent.scrollHeight;
}
}
},
// 信息日志
info: function(message) {
var _a;
console.log(`[信息] ${message}`);
(_a = EducationHelper.Status) == null ? void 0 : _a.setLastAction(message);
},
// 操作日志
action: function(message) {
var _a;
console.log(`[操作] ${message}`);
(_a = EducationHelper.Status) == null ? void 0 : _a.setLastAction(message);
},
// 成功日志
success: function(message) {
var _a;
console.log(`[成功] ${message}`);
(_a = EducationHelper.Status) == null ? void 0 : _a.setLastAction(message);
},
// 警告日志
warn: function(message) {
var _a;
console.warn(`[警告] ${message}`);
(_a = EducationHelper.Status) == null ? void 0 : _a.setLastAction(`警告: ${message}`);
},
// 错误日志
error: function(message, error) {
var _a;
if (error) {
console.error(`[错误] ${message}`, error);
} else {
console.error(`[错误] ${message}`);
}
(_a = EducationHelper.Status) == null ? void 0 : _a.setLastAction(`错误: ${message}`);
}
};
EducationHelper.Runtime = {
setTimeout: function(callback, delay) {
return window.setTimeout(callback, delay);
},
clearTimeout: function(timerId) {
if (timerId) {
window.clearTimeout(timerId);
}
},
setInterval: function(callback, delay) {
return window.setInterval(callback, delay);
},
clearInterval: function(timerId) {
if (timerId) {
window.clearInterval(timerId);
}
},
observe: function(target, options, callback) {
if (!target || typeof MutationObserver === "undefined") {
return null;
}
const observer = new MutationObserver(callback);
observer.observe(target, options);
return observer;
},
disconnectObserver: function(observer) {
if (observer) {
observer.disconnect();
}
}
};
const ROUTES = {
evaluationPage: "teachingEvaluation/evaluationPage",
// 单个评估页
evaluationList: ["teachingEvaluation/teachingEvaluation/index", "teachingEvaluation/evaluation/index"],
// 评估列表页
scorePage: "integratedQuery/scoreQuery",
// 成绩查询页
schedulePage: ["thisSemesterCurriculum", "calendarSemesterCurriculum"],
// 课表页
planPage: "integratedQuery/planCompletion",
// 培养方案页
coursePage: "courseSelect"
// 选课页(排除课表)
};
const URLS = {
schedule: "/student/courseSelect/thisSemesterCurriculum/index",
planCompletion: "/student/integratedQuery/planCompletion/index",
scoreQuery: "/student/integratedQuery/scoreQuery/schemeScores/index",
evaluationList: "/student/teachingEvaluation/evaluation/index",
courseSelect: "/student/courseSelect/courseSelect/index"
};
const SELECTORS = {
// 培养方案页
planTabOne: "#one",
// 概览 Tab
planInfobox: ".infobox",
// 统计卡片
planInfoboxNum: ".infobox-data-number",
// 卡片数字
planInfoboxLabel: ".infobox-content",
// 卡片标签
planInfoboxPercent: ".percent",
// 百分比
planInfoboxPercentLabel: ".infobox-text",
// 百分比标签
planInfoboxSmall: ".infobox-small",
// 小卡片
planTreeNodes: ".node_name",
// zTree 课组节点
planTreeContainer: "#treeDemo",
// zTree 容器
planTreeRootNodes: "#treeDemo > li > a .node_name",
// zTree 根节点
// 成绩页
scoreTable: [
// 成绩表格(按优先级尝试)
"#page-content-template table.table-striped",
"#timeline table.table-striped",
"table.table-striped.table-bordered",
".scrollspy-example table.table-striped"
],
// 评估页
evalJqTextarea: "#page-content-template > div > div > div.widget-content > form > div > table > tbody > tr:nth-child(25) > td > div > textarea"
};
const PATTERNS = {
groupKeyword: "最低修读学分",
// 课组节点关键词
completedIcon: "fa-check-square-o",
// 已完成课组图标 class
minCredit: /最低修读学分[::]?([\d.]+)/,
passedCredit: /通过学分[::]?([\d.]+)/,
failedCourses: /未及格课程门数[::]?(\d+)/,
missingCourses: /必修课缺修门数[::]?(\d+)/,
groupName: /\s*(.+?)\(/
// 课组名提取(括号前的文本)
};
EducationHelper.Config = {
// 引用集中配置区
routes: ROUTES,
urls: URLS,
selectors: SELECTORS,
patterns: PATTERNS,
// 全局状态
state: {
targetCourses: [],
matchedCourses: [],
timer: null,
autoMode: false,
autoClickEvaluationEnabled: false,
autoClickStopped: false,
dryRunMode: false,
courseMonitorEnabled: false,
courseMonitorInterval: 5,
courseMonitorTimer: null,
selectedOption: "A",
autoSubmitEnabled: true,
debugMode: false,
uiFollowPage: true,
panelPosition: null
},
// 页面类型
pageType: {
currentPageUrl: window.location.href,
isCoursePage: false,
isEvaluationPage: false,
isEvaluationListPage: false,
isScorePage: false,
isSchedulePage: false,
isHomePage: false,
isPlanPage: false
},
// 时间设置
timers: {
autoClickEvaluationDelay: 1e4,
autoSubmitDelay: 12e4
},
// 内容设置
content: {
evaluationComment: "上课有热情,积极解决学生问题,很好的老师!!",
evaluationTemplates: [
"老师上课讲解清晰,条理分明,课堂气氛活跃,能够很好地调动同学们的学习积极性。",
"课程内容丰富充实,老师备课认真,教学态度端正,对学生耐心负责,值得称赞。",
"老师教学经验丰富,善于用生动的例子帮助理解抽象概念,课堂内容深入浅出。",
"教学方法灵活多样,注重理论联系实际,拓宽了我们的知识面和视野,受益匪浅。",
"老师对待教学认真负责,课上互动积极,课下答疑耐心,是一位优秀的老师。",
"上课有热情,积极解决学生问题,讲课逻辑清晰,重点突出,学到了很多知识。",
"老师授课风格独特,课堂生动有趣,能够激发我们的学习兴趣和思考能力。",
"课程安排合理,教学进度适当,老师注重培养我们的实践能力,教学效果很好。",
"老师为人和蔼可亲,上课幽默风趣,与同学们关系融洽,教学水平高,值得尊敬。",
"课堂氛围轻松活跃,老师鼓励学生表达观点,培养了我们的独立思考能力,感谢老师的付出。"
],
useRandomTemplate: false
},
// 初始化配置
init: function() {
this.pageType.currentPageUrl = window.location.href;
var url = this.pageType.currentPageUrl;
var r = this.routes;
var urlHas = function(keywords) {
if (Array.isArray(keywords)) return keywords.some(function(k) {
return url.includes(k);
});
return url.includes(keywords);
};
this.pageType.isEvaluationPage = urlHas(r.evaluationPage);
this.pageType.isEvaluationListPage = urlHas(r.evaluationList);
this.pageType.isScorePage = urlHas(r.scorePage);
this.pageType.isSchedulePage = urlHas(r.schedulePage);
this.pageType.isPlanPage = urlHas(r.planPage);
this.pageType.isHomePage = /\/(student(\/index)?)?\/?([?#].*)?$/.test(url) && !this.pageType.isEvaluationPage && !this.pageType.isEvaluationListPage && !this.pageType.isScorePage && !this.pageType.isSchedulePage;
this.pageType.isCoursePage = urlHas(r.coursePage) && !this.pageType.isSchedulePage;
this.loadSavedSettings();
return this;
},
// 保存设置到localStorage
saveSettings: function() {
try {
EducationHelper.Storage.set("settings", {
autoMode: this.state.autoMode,
autoClickEvaluationEnabled: this.state.autoClickEvaluationEnabled,
autoSubmitEnabled: this.state.autoSubmitEnabled,
dryRunMode: this.state.dryRunMode,
selectedOption: this.state.selectedOption,
debugMode: this.state.debugMode,
uiFollowPage: this.state.uiFollowPage,
panelPosition: this.state.panelPosition,
evaluationComment: this.content.evaluationComment,
useRandomTemplate: this.content.useRandomTemplate
});
console.log("[设置] 已保存设置");
} catch (e) {
console.warn("[警告] 保存设置失败:", e);
}
},
// 从localStorage加载设置
loadSavedSettings: function() {
try {
const stored = EducationHelper.Storage.get("settings", {});
if (stored.autoMode === true) {
this.state.autoMode = true;
console.log("[检测] 从localStorage检测到全自动模式已启用");
}
[
[this.state, "autoClickEvaluationEnabled"],
[this.state, "autoSubmitEnabled"],
[this.state, "dryRunMode"],
[this.state, "debugMode"],
[this.state, "uiFollowPage"],
[this.content, "useRandomTemplate"]
].forEach(([target, key]) => {
if (typeof stored[key] === "boolean") target[key] = stored[key];
});
if (stored.panelPosition) this.state.panelPosition = stored.panelPosition;
if (stored.evaluationComment) this.content.evaluationComment = stored.evaluationComment;
if (stored.selectedOption) this.state.selectedOption = stored.selectedOption;
} catch (e) {
console.warn("[警告] 读取设置失败:", e);
}
}
};
const BrowserHelperCore = {
selectors: {
helperContainer: "#qqhruHelperUI",
helperStatusBody: "#helperStatusBody",
evaluationAction: 'button, a, input[type="button"], input[type="submit"]',
evaluationScope: "#page-content-template, #page-content, .page-content, .main-content, .widget-main, .widget-body, .widget-box, .tab-content, .content",
evaluationIgnore: "#qqhruHelperUI, #sidebar, .sidebar, .nav-list, .sidebar-menu, #breadcrumbs, .breadcrumbs, .breadcrumb, .page-header, .navbar, .ace-nav, .nav-tabs, .pagination, .footer",
evaluationContainer: "tr, li, .list-item, .evaluation-item, .widget-main, .widget-body, .row",
evaluationInputs: 'textarea, .ace, input[type="radio"], input[type="checkbox"]',
evaluationChoices: 'input[type="radio"], input[type="checkbox"], .ace',
submitAction: 'button, input[type="submit"], input[type="button"], a.btn',
confirmAction: '.modal-footer .btn, .layui-layer-btn .layui-layer-btn0, .dialog-footer .btn, .layui-layer-btn a, button, a.btn, input[type="button"], input[type="submit"]',
backAction: 'a[href*="index"], button, a',
courseRows: "tr",
courseCheckbox: 'input[type="checkbox"]'
},
normalizeText: function(text) {
return String(text || "").replace(/\s+/g, " ").trim();
},
isEvaluationActionText: function(text) {
const normalizedText = this.normalizeText(text);
return (normalizedText.includes("评估") || normalizedText.includes("评价")) && !normalizedText.includes("统计") && !normalizedText.includes("提交") && !normalizedText.includes("保存");
},
isCompletedText: function(text) {
const normalizedText = this.normalizeText(text);
return /(已评|已完成|完成)/.test(normalizedText) && !/(未评|待评)/.test(normalizedText);
},
matchCourseTarget: function(target, rowText, numberTokens = []) {
const normalizedTarget = this.normalizeText(target);
const normalizedRowText = this.normalizeText(rowText);
if (!normalizedTarget) {
return false;
}
if (/^\d+$/.test(normalizedTarget)) {
return numberTokens.includes(normalizedTarget);
}
return normalizedRowText.includes(normalizedTarget);
},
scoreSubmitCandidate: function(candidate) {
const text = this.normalizeText(candidate.text);
const type = candidate.type || "";
const className = candidate.className || "";
const rectTop = candidate.rectTop || 0;
const viewportHeight = candidate.viewportHeight || 1e3;
let score = 0;
if (text === "提交") {
score += 20;
}
if (text.includes("提交")) {
score += 10;
}
if (text.includes("保存并提交") || text.includes("确认提交")) {
score += 6;
}
if (type === "submit") {
score += 6;
}
if (/btn-danger|btn-primary|layui-btn|submit/i.test(className)) {
score += 4;
}
if (rectTop > viewportHeight / 2) {
score += 3;
}
return score;
},
scoreBackCandidate: function(candidate) {
const text = this.normalizeText(candidate.text);
const href = candidate.href || "";
let score = 0;
if (text.includes("评估列表")) {
score += 14;
}
if (text.includes("返回")) {
score += 12;
}
if (text.includes("列表")) {
score += 10;
}
if (href.includes("teachingEvaluation")) {
score += 4;
}
if (href.includes("index")) {
score += 3;
}
return score;
},
pickBestBackCandidate: function(candidates = []) {
var _a;
return ((_a = candidates.map((candidate) => ({
candidate,
score: this.scoreBackCandidate(candidate)
})).sort((a, b) => b.score - a.score)[0]) == null ? void 0 : _a.candidate) || null;
}
};
EducationHelper.escapeHtml = function(str) {
if (typeof str !== "string") return String(str);
const map = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
return str.replace(/[&<>"']/g, (c) => map[c]);
};
EducationHelper.Utils = {
normalizeText: function(text) {
return BrowserHelperCore.normalizeText(text);
},
getElementText: function(element) {
if (!element) {
return "";
}
return this.normalizeText(
element.innerText || element.textContent || element.value || ""
);
},
isIgnoredEvaluationElement: function(element) {
if (!element) {
return false;
}
return !!element.closest(BrowserHelperCore.selectors.evaluationIgnore);
},
getEvaluationSearchRoot: function() {
const defaultRoot = document.body || document.documentElement;
const candidates = Array.from(document.querySelectorAll(BrowserHelperCore.selectors.evaluationScope)).filter((element) => this.isElementVisible(element)).filter((element) => !this.isIgnoredEvaluationElement(element));
if (candidates.length === 0) {
return defaultRoot;
}
return candidates.map((element) => {
const rect = element.getBoundingClientRect();
return {
element,
score: rect.width * rect.height + element.querySelectorAll(BrowserHelperCore.selectors.evaluationAction).length * 5e3
};
}).sort((a, b) => b.score - a.score)[0].element;
},
hasEvaluationClosedNotice: function(root) {
const searchRoot = root || this.getEvaluationSearchRoot();
const alerts = Array.from(searchRoot.querySelectorAll(".alert, .alert-success, .alert-warning, .message, .no-data, .empty, .widget-body"));
return alerts.some(
(element) => this.isElementVisible(element) && this.getElementText(element).includes("评估开关已关闭")
);
},
isElementVisible: function(element) {
if (!element || !element.isConnected) {
return false;
}
const style = window.getComputedStyle(element);
const rect = element.getBoundingClientRect();
return style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0;
},
isElementDisabled: function(element) {
return !!(!element || element.disabled || element.getAttribute("aria-disabled") === "true" || element.classList.contains("disabled"));
},
clickElement: function(element) {
if (!element) {
return false;
}
try {
element.scrollIntoView({ behavior: "smooth", block: "center" });
} catch (error) {
}
try {
element.click();
return true;
} catch (error) {
EducationHelper.Logger.warn("直接点击失败,尝试派发事件");
}
try {
const event = new MouseEvent("click", {
bubbles: true,
cancelable: true,
view: window
});
element.dispatchEvent(event);
return true;
} catch (error) {
EducationHelper.Logger.error("点击元素失败", error);
return false;
}
},
findClickableByText: function(keywords, selectors = 'button, a, input[type="button"], input[type="submit"]') {
const normalizedKeywords = keywords.map((keyword) => this.normalizeText(keyword));
const candidates = Array.from(document.querySelectorAll(selectors)).filter((element) => this.isElementVisible(element));
return candidates.find((element) => {
const text = this.getElementText(element);
return normalizedKeywords.some((keyword) => text.includes(keyword));
}) || null;
},
handleDryRun: function(targets, description) {
if (!EducationHelper.Config.state.dryRunMode) {
return false;
}
EducationHelper.Preview.highlightTargets(targets, description);
EducationHelper.Status.setNextAction("关闭演练模式后可执行真实点击/提交");
EducationHelper.Status.setLastAction(`演练模式: ${description}`);
return true;
},
waitFor: function(checker, options = {}) {
const timeout = options.timeout || 8e3;
const interval = options.interval || 150;
const root = options.root || document.body || document.documentElement;
return new Promise((resolve, reject) => {
let settled = false;
let observer = null;
let intervalTimer = null;
let timeoutTimer = null;
const cleanup = () => {
EducationHelper.Runtime.disconnectObserver(observer);
EducationHelper.Runtime.clearInterval(intervalTimer);
EducationHelper.Runtime.clearTimeout(timeoutTimer);
};
const finish = (result) => {
if (settled) {
return;
}
settled = true;
cleanup();
resolve(result);
};
const fail = () => {
if (settled) {
return;
}
settled = true;
cleanup();
reject(new Error(options.message || "等待元素超时"));
};
const runCheck = () => {
try {
const result = checker();
if (result) {
finish(result);
}
} catch (error) {
EducationHelper.Logger.debug("waitFor 检查失败", error);
}
};
runCheck();
if (settled) {
return;
}
intervalTimer = EducationHelper.Runtime.setInterval(runCheck, interval);
timeoutTimer = EducationHelper.Runtime.setTimeout(fail, timeout);
observer = EducationHelper.Runtime.observe(root, {
childList: true,
subtree: true,
attributes: true
}, runCheck);
});
}
};
EducationHelper.Status = {
state: {
nextAction: "等待用户操作",
lastAction: "脚本已就绪",
recognized: "等待识别"
},
generatePanel: function() {
return `
`;
},
getPageLabel: function() {
if (EducationHelper.Config.pageType.isCoursePage) {
return "抢课页";
}
if (EducationHelper.Config.pageType.isEvaluationPage) {
return "单个评估页";
}
if (EducationHelper.Config.pageType.isEvaluationListPage) {
return "评估列表页";
}
if (EducationHelper.Config.pageType.isScorePage) {
return "成绩查询页";
}
if (EducationHelper.Config.pageType.isSchedulePage) {
return "课表页";
}
if (EducationHelper.Config.pageType.isHomePage) {
return "首页";
}
if (EducationHelper.Config.pageType.isPlanPage) {
return "培养方案页";
}
return "未知页面";
},
getModeLabel: function() {
if (EducationHelper.Config.state.autoMode) {
return "全自动";
}
if (EducationHelper.Config.state.autoClickEvaluationEnabled) {
return "自动点击评估";
}
return "手动";
},
getDryRunLabel: function() {
return EducationHelper.Config.state.dryRunMode ? "演练模式(不执行)" : "实际执行";
},
getModeSummary: function() {
return `${this.getModeLabel()} · ${this.getDryRunLabel()}`;
},
getWaitPolicy: function() {
if (EducationHelper.Config.pageType.isEvaluationPage || EducationHelper.Config.pageType.isEvaluationListPage) {
return "系统要求等待 120 秒,脚本不会跳过";
}
return "按页面实时操作,无强制等待";
},
getRecognizedSummary: function() {
var _a, _b, _c, _d;
if (EducationHelper.Config.pageType.isCoursePage) {
return `待选 ${EducationHelper.Config.state.targetCourses.length} / 已匹配 ${EducationHelper.Config.state.matchedCourses.length}`;
}
if (EducationHelper.Config.pageType.isEvaluationListPage) {
const total = ((_b = (_a = EducationHelper.EvaluationList) == null ? void 0 : _a.progress) == null ? void 0 : _b.total) || 0;
const completed = ((_d = (_c = EducationHelper.EvaluationList) == null ? void 0 : _c.progress) == null ? void 0 : _d.completed) || 0;
const pending = Math.max(total - completed, 0);
return `待评 ${pending} / 总数 ${total}`;
}
if (EducationHelper.Config.pageType.isEvaluationPage) {
const optionCount = document.querySelectorAll(BrowserHelperCore.selectors.evaluationChoices).length;
const textareaCount = document.querySelectorAll("textarea").length;
return `题项 ${optionCount} / 文本框 ${textareaCount}`;
}
if (EducationHelper.Config.pageType.isPlanPage) {
var planData = EducationHelper.PlanCompletion._data;
if (planData && planData.groups.length > 0) {
var cg = planData.groups.filter(function(g) {
return g.completed;
}).length;
var ig = planData.groups.length - cg;
return "课组 " + planData.groups.length + " · 完成 " + cg + " · 未完成 " + ig;
}
return "正在分析...";
}
return this.state.recognized;
},
setNextAction: function(value) {
this.state.nextAction = value;
this.render();
},
setLastAction: function(value) {
this.state.lastAction = value;
this.render();
},
setRecognized: function(value) {
this.state.recognized = value;
this.render();
},
syncFromState: function() {
this.render();
},
render: function() {
const container = document.querySelector(BrowserHelperCore.selectors.helperStatusBody);
if (!container) {
return;
}
const recognizedSummary = this.getRecognizedSummary() || this.state.recognized;
const rows = [
{ label: "页面", value: this.getPageLabel() },
{ label: "模式", value: this.getModeSummary() },
{ label: "识别", value: recognizedSummary, wide: true },
{ label: "下一步", value: this.state.nextAction, wide: true }
];
container.innerHTML = rows.map(({ label, value, wide }) => `
${EducationHelper.escapeHtml(label)}
${EducationHelper.escapeHtml(value)}
`).join("") + `
最近动作:${EducationHelper.escapeHtml(this.state.lastAction)}
`;
}
};
EducationHelper.Preview = {
highlightedTargets: /* @__PURE__ */ new Set(),
clear: function() {
this.highlightedTargets.forEach((element) => {
if (element == null ? void 0 : element.classList) {
element.classList.remove("helper-preview-outline");
}
});
this.highlightedTargets.clear();
},
highlightTargets: function(targets, description) {
var _a;
const targetList = (Array.isArray(targets) ? targets : [targets]).filter(Boolean);
this.clear();
targetList.forEach((target) => {
if (target.classList) {
target.classList.add("helper-preview-outline");
}
this.highlightedTargets.add(target);
});
if ((_a = targetList[0]) == null ? void 0 : _a.scrollIntoView) {
try {
targetList[0].scrollIntoView({ behavior: "smooth", block: "center" });
} catch (error) {
EducationHelper.Logger.debug("预览滚动失败", error);
}
}
EducationHelper.UI.showMessage(`
演练模式
${description}
`, "warning", 2200);
}
};
EducationHelper.Notification = {
send: function(title, text, options = {}) {
try {
if (typeof GM_notification === "function") {
GM_notification({
title: "齐大教务助手 - " + title,
text,
timeout: options.timeout || 5e3,
onclick: options.onclick || function() {
}
});
}
} catch (error) {
console.warn("[通知] GM_notification 不可用:", error);
}
},
evaluationComplete: function(completed, total) {
this.send("评估完成", `已完成全部 ${total} 项教学评估!`);
},
evaluationProgress: function(completed, total) {
this.send("评估进度", `已完成 ${completed}/${total} 项评估`);
},
courseGrabbed: function(courseName) {
this.send("抢课成功", `课程「${courseName}」已成功选中!`);
},
scriptError: function(message) {
this.send("运行异常", message, { timeout: 8e3 });
}
};
const cssRules = `
/* 样式作用域限定在助手容器内,避免污染宿主页面 */
#qqhruHelperUI, #qqhruHelperUI * {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
}
/* iOS风格按钮 */
.ui-button {
background: #2563EB;
color: white;
border: none;
padding: 12px 16px;
cursor: pointer;
width: 100%;
margin-bottom: 12px;
border-radius: 12px;
transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
font-weight: 600;
font-size: 16px;
letter-spacing: -0.24px;
box-shadow: 0 2px 8px rgba(0, 122, 255, 0.25);
position: relative;
overflow: hidden;
}
.ui-button::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparent;
border-radius: 12px;
pointer-events: none;
}
.ui-button:hover {
background: #1D4ED8;
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(0, 122, 255, 0.35);
}
.ui-button:active {
transform: scale(0.96);
box-shadow: 0 1px 4px rgba(0, 122, 255, 0.2);
}
.ui-button:disabled {
background: #E5E5EA;
color: #8E8E93;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* iOS风格输入框 */
.ui-input {
width: 100%;
padding: 12px 16px;
margin-bottom: 12px;
border: 1px solid #E5E5EA;
border-radius: 12px;
box-sizing: border-box;
font-size: 16px;
background: #FFFFFF;
transition: all 0.2s ease;
-webkit-appearance: none;
}
.ui-input:focus {
outline: none;
border-color: #007AFF;
box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1);
}
/* iOS风格标签 */
.ui-label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #1C1C1E;
font-size: 15px;
letter-spacing: -0.24px;
}
/* iOS风格面板 */
.ui-panel {
border: none;
border-radius: 16px;
padding: 16px;
margin-bottom: 16px;
background: #F2F2F7;
max-height: 140px;
overflow-y: auto;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.ui-panel-title {
font-weight: 700;
margin-bottom: 8px;
color: #007AFF;
font-size: 16px;
letter-spacing: -0.32px;
}
/* iOS风格标签页 */
.ui-tabs {
display: flex;
margin-bottom: 16px;
background: #F2F2F7;
border-radius: 12px;
padding: 4px;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
}
.ui-tab {
flex: 1;
padding: 10px 16px;
cursor: pointer;
background: transparent;
border: none;
border-radius: 8px;
margin: 0;
font-weight: 500;
color: #8E8E93;
transition: all 0.2s ease;
text-align: center;
font-size: 15px;
}
.ui-tab.active {
background: #FFFFFF;
color: #007AFF;
font-weight: 600;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.ui-tab-content {
display: none;
}
.ui-tab-content.active {
display: block;
}
/* iOS风格删除按钮 */
.delete-btn {
background: #FF3B30;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
padding: 6px 12px;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
}
.delete-btn:hover {
background: #D70015;
transform: scale(0.95);
}
/* 课程项目 */
.course-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #E5E5EA;
font-size: 15px;
}
.course-item:last-child {
border-bottom: none;
}
/* iOS风格复选框容器 */
.ui-checkbox-container {
display: flex;
align-items: center;
margin-bottom: 16px;
padding: 12px 16px;
background: #F2F2F7;
border-radius: 12px;
}
.ui-checkbox {
margin-right: 12px;
width: 20px;
height: 20px;
accent-color: #007AFF;
}
/* 选项容器 */
.option-container {
margin-bottom: 16px;
background: #F2F2F7;
border-radius: 12px;
padding: 8px;
}
.option-row {
margin-bottom: 0;
}
.radio-label {
display: flex;
align-items: center;
cursor: pointer;
padding: 12px 16px;
border-radius: 8px;
transition: background-color 0.2s ease;
font-size: 15px;
font-weight: 500;
}
.radio-label:hover {
background: rgba(0, 122, 255, 0.1);
}
.radio-label input {
margin-right: 12px;
width: 18px;
height: 18px;
accent-color: #007AFF;
}
/* iOS风格计时器显示 */
.timer-display {
background: #EFF6FF;
color: #2563EB;
padding: 12px 16px;
border-radius: 12px;
text-align: center;
margin-bottom: 16px;
font-weight: 600;
font-size: 16px;
display: none;
box-shadow: 0 2px 8px rgba(52, 199, 89, 0.25);
}
.helper-preview-outline {
outline: 3px solid #FF9500 !important;
outline-offset: 3px;
box-shadow: 0 0 0 6px rgba(255, 149, 0, 0.18) !important;
transition: box-shadow 0.2s ease, outline-color 0.2s ease;
}
.helper-status-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.helper-status-row {
display: block;
padding: 8px 10px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(36, 91, 145, 0.08);
}
.helper-status-row.wide {
grid-column: 1 / -1;
}
.helper-status-label {
display: block;
margin-bottom: 4px;
font-size: 11px;
font-weight: 700;
color: #245B91;
}
.helper-status-value {
font-size: 12px;
line-height: 1.5;
color: #1C1C1E;
word-break: break-word;
}
#qqhruHelperUI {
--helper-bg: #ffffff;
--helper-surface: #f8fafc;
--helper-surface-soft: #f1f5f9;
--helper-border: #dbe3ec;
--helper-text: #172334;
--helper-muted: #66758a;
--helper-primary: #2563eb;
--helper-primary-hover: #1d4ed8;
--helper-primary-soft: #eff6ff;
--helper-warning-bg: #fff7ed;
--helper-warning-border: #fed7aa;
--helper-warning-text: #c2410c;
--helper-success-bg: #f0fdf4;
--helper-success-border: #bbf7d0;
--helper-success-text: #166534;
--helper-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
}
#qqhruHelperUI {
width: 300px !important;
background: var(--helper-bg) !important;
border: 1px solid var(--helper-border) !important;
border-radius: 14px !important;
box-shadow: var(--helper-shadow) !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif !important;
}
#dragBar {
background: var(--helper-bg) !important;
color: var(--helper-text) !important;
border-bottom: 1px solid var(--helper-border) !important;
padding: 12px 14px !important;
}
#dragBar span {
font-size: 15px !important;
font-weight: 700 !important;
letter-spacing: 0 !important;
}
#dragBar button {
background: var(--helper-surface) !important;
color: var(--helper-text) !important;
border: 1px solid var(--helper-border) !important;
border-radius: 10px !important;
box-shadow: none !important;
}
#dragBar button:hover {
background: var(--helper-surface-soft) !important;
}
#closeBtn {
background: #fff1f2 !important;
color: #dc2626 !important;
border-color: #fecdd3 !important;
}
#closeBtn:hover {
background: #ffe4e6 !important;
}
#uiContent {
background: var(--helper-bg) !important;
padding: 14px !important;
}
.ui-tabs {
background: transparent !important;
padding: 0 !important;
box-shadow: none !important;
margin-bottom: 12px !important;
}
.ui-tab {
background: var(--helper-surface) !important;
color: var(--helper-muted) !important;
border: 1px solid var(--helper-border) !important;
border-radius: 10px !important;
box-shadow: none !important;
font-weight: 600 !important;
padding: 8px 12px !important;
font-size: 14px !important;
}
.ui-tab.active {
background: var(--helper-primary-soft) !important;
color: var(--helper-primary) !important;
border-color: #cfe0ff !important;
}
.ui-button {
background: var(--helper-primary) !important;
color: #ffffff !important;
border: 1px solid transparent !important;
border-radius: 10px !important;
box-shadow: none !important;
padding: 10px 14px !important;
font-size: 14px !important;
letter-spacing: 0 !important;
}
.ui-button:hover {
background: var(--helper-primary-hover) !important;
transform: none !important;
box-shadow: none !important;
}
.ui-button::before {
display: none !important;
}
.ui-button:active {
transform: none !important;
box-shadow: none !important;
}
.ui-button:disabled,
#stopScript {
background: var(--helper-surface) !important;
color: var(--helper-muted) !important;
border: 1px solid var(--helper-border) !important;
box-shadow: none !important;
}
.ui-input,
textarea.ui-input {
border: 1px solid var(--helper-border) !important;
border-radius: 10px !important;
background: var(--helper-bg) !important;
box-shadow: none !important;
font-size: 14px !important;
}
.ui-label {
margin-bottom: 6px !important;
font-size: 13px !important;
font-weight: 600 !important;
color: var(--helper-text) !important;
letter-spacing: 0 !important;
}
.ui-panel,
.ui-checkbox-container,
.option-container {
background: var(--helper-surface) !important;
border: 1px solid var(--helper-border) !important;
border-radius: 12px !important;
box-shadow: none !important;
}
.ui-panel {
padding: 10px !important;
margin-bottom: 10px !important;
}
.ui-checkbox-container {
display: grid !important;
grid-template-columns: 18px 1fr !important;
column-gap: 10px !important;
align-items: center !important;
padding: 9px 10px !important;
}
.ui-checkbox-container.compact {
margin-bottom: 0 !important;
padding: 8px 10px !important;
}
.ui-checkbox {
width: 18px !important;
height: 18px !important;
margin: 0 !important;
vertical-align: middle !important;
accent-color: var(--helper-primary) !important;
align-self: center !important;
justify-self: start !important;
}
.ui-checkbox-container .ui-label {
display: block !important;
margin: 0 !important;
font-size: 14px !important;
line-height: 1.45 !important;
color: var(--helper-text) !important;
cursor: pointer !important;
}
.option-container {
padding: 4px !important;
}
.ui-panel-title {
color: var(--helper-text) !important;
font-size: 14px !important;
margin-bottom: 8px !important;
letter-spacing: 0 !important;
}
.radio-label:hover {
background: var(--helper-primary-soft) !important;
}
.helper-status-row {
background: var(--helper-surface) !important;
border-color: var(--helper-border) !important;
padding: 7px 9px !important;
border-radius: 10px !important;
}
.helper-status-label {
font-size: 12px !important;
color: var(--helper-muted) !important;
}
.helper-status-value {
font-size: 12px !important;
color: var(--helper-text) !important;
}
.helper-status-note {
margin-top: 2px !important;
grid-column: 1 / -1 !important;
padding: 6px 2px 0 !important;
font-size: 12px !important;
line-height: 1.5 !important;
color: var(--helper-muted) !important;
}
.ui-disclosure {
margin-bottom: 10px !important;
border: 1px solid var(--helper-border) !important;
border-radius: 12px !important;
background: var(--helper-surface) !important;
overflow: hidden !important;
}
.ui-disclosure summary {
list-style: none !important;
cursor: pointer !important;
padding: 10px 12px !important;
font-size: 13px !important;
font-weight: 600 !important;
color: var(--helper-text) !important;
}
.ui-disclosure summary::-webkit-details-marker {
display: none !important;
}
.ui-disclosure[open] summary {
border-bottom: 1px solid var(--helper-border) !important;
}
.ui-disclosure-body {
padding: 10px 12px !important;
font-size: 12px !important;
line-height: 1.6 !important;
color: var(--helper-muted) !important;
}
.timer-display,
#countdownDiv {
background: var(--helper-primary-soft) !important;
color: var(--helper-primary) !important;
border: 1px solid #cfe0ff !important;
border-radius: 10px !important;
box-shadow: none !important;
}
#qqhruHelperUI .ui-panel[style*="#FFF8E1"] {
background: var(--helper-warning-bg) !important;
border-color: var(--helper-warning-border) !important;
}
#qqhruHelperUI .ui-panel[style*="#FFF8E1"] .ui-panel-title,
#qqhruHelperUI .ui-panel[style*="#FFF8E1"] div {
color: var(--helper-warning-text) !important;
}
#evaluationProgress,
#debugInfo {
background: var(--helper-surface) !important;
}
`;
function addStyles() {
if (document.getElementById("qqhruHelperStyles")) return;
const style = document.createElement("style");
style.id = "qqhruHelperStyles";
style.textContent = cssRules;
document.head.appendChild(style);
}
EducationHelper.UI = {
elements: {
container: null,
dragBar: null,
content: null
},
getDragBounds: function() {
const container = this.elements.container;
const dragBar = this.elements.dragBar;
if (!container) {
return { minX: 0, maxX: 0, minY: 0, maxY: 0 };
}
const width = container.offsetWidth || 320;
const visibleWidth = Math.min(96, Math.max(64, Math.round(width * 0.35)));
const dragBarHeight = (dragBar == null ? void 0 : dragBar.offsetHeight) || 60;
return {
minX: Math.min(0, visibleWidth - width),
maxX: Math.max(0, window.innerWidth - visibleWidth),
minY: 0,
maxY: Math.max(0, window.innerHeight - dragBarHeight)
};
},
clampPanelPosition: function(left, top) {
const bounds = this.getDragBounds();
return {
left: Math.min(Math.max(left, bounds.minX), bounds.maxX),
top: Math.min(Math.max(top, bounds.minY), bounds.maxY)
};
},
applyPanelPosition: function(left, top) {
if (!this.elements.container) return { left, top };
const position = this.clampPanelPosition(left, top);
this.elements.container.style.left = `${Math.round(position.left)}px`;
this.elements.container.style.top = `${Math.round(position.top)}px`;
this.elements.container.style.right = "auto";
return position;
},
syncViewportConstraints: function() {
var _a;
if (!this.elements.container || !this.elements.content) return;
const viewportPadding = 20;
const maxHeight = Math.max(320, window.innerHeight - viewportPadding * 2);
const dragBarHeight = ((_a = this.elements.dragBar) == null ? void 0 : _a.offsetHeight) || 60;
const contentMaxHeight = Math.max(180, maxHeight - dragBarHeight);
this.elements.container.style.maxHeight = `${Math.round(maxHeight)}px`;
this.elements.content.style.maxHeight = `${Math.round(contentMaxHeight)}px`;
this.elements.content.style.overflowY = "auto";
this.elements.content.style.overscrollBehavior = "contain";
this.elements.content.style.paddingBottom = "28px";
},
// 公共「更多设置」区块
generateSettingsFooter: function() {
var cfg = EducationHelper.Config;
var esc = EducationHelper.escapeHtml;
return '更多设置 ' + this._settingsCheckbox("globalUseRandom", "随机评语模板", cfg.content.useRandomTemplate) + this._settingsCheckbox("globalDryRun", "演练模式(只识别,不操作)", cfg.state.dryRunMode) + this._settingsCheckbox("globalAutoSubmit", "自动提交评价", cfg.state.autoSubmitEnabled) + this._settingsCheckbox("debugMode", "调试模式", cfg.state.debugMode) + '
导出设置 导入设置
快捷键:Ctrl+Shift+H 显示/隐藏 · Ctrl+Shift+S 启停自动 · Esc 取消
';
},
_settingsCheckbox: function(id, label, checked) {
return '' + label + "
";
},
bindSettingsFooter: function() {
var save = function() {
EducationHelper.Config.saveSettings();
};
var optionRadios = document.querySelectorAll('input[name="globalOption"]');
optionRadios.forEach(function(radio) {
radio.addEventListener("change", function() {
EducationHelper.Config.state.selectedOption = this.value;
EducationHelper.Logger.action("默认评估选项: " + this.value);
save();
optionRadios.forEach(function(r) {
var lbl = r.parentElement;
var active = r.checked;
lbl.style.background = active ? "var(--helper-primary-soft)" : "var(--helper-surface)";
lbl.style.color = active ? "var(--helper-primary)" : "var(--helper-muted)";
lbl.style.borderColor = active ? "#cfe0ff" : "var(--helper-border)";
});
});
});
var commentEl = document.getElementById("globalEvalComment");
if (commentEl) {
commentEl.addEventListener("input", function() {
EducationHelper.Config.content.evaluationComment = this.value;
save();
});
}
var randomEl = document.getElementById("globalUseRandom");
if (randomEl) {
randomEl.addEventListener("change", function() {
EducationHelper.Config.content.useRandomTemplate = this.checked;
EducationHelper.Logger.action("随机评语: " + (this.checked ? "已启用" : "已禁用"));
save();
});
}
var dryRunEl = document.getElementById("globalDryRun");
if (dryRunEl) {
dryRunEl.addEventListener("change", function() {
EducationHelper.Config.state.dryRunMode = this.checked;
EducationHelper.Logger.action("演练模式: " + (this.checked ? "已启用" : "已关闭"));
save();
EducationHelper.Status.syncFromState();
});
}
var autoSubmitEl = document.getElementById("globalAutoSubmit");
if (autoSubmitEl) {
autoSubmitEl.addEventListener("change", function() {
EducationHelper.Config.state.autoSubmitEnabled = this.checked;
EducationHelper.Logger.action("自动提交: " + (this.checked ? "已启用" : "已禁用"));
save();
});
}
var debugEl = document.getElementById("debugMode");
if (debugEl) {
debugEl.addEventListener("change", function() {
EducationHelper.Config.state.debugMode = this.checked;
EducationHelper.Logger.action("调试模式: " + (this.checked ? "已启用" : "已禁用"));
save();
var debugPanel = document.getElementById("debugInfo");
if (debugPanel) debugPanel.style.display = this.checked ? "block" : "none";
if (this.checked) {
EducationHelper.Logger.debug("调试模式已启用");
EducationHelper.Logger.debug("当前URL: " + window.location.href);
EducationHelper.Logger.debug("页面标题: " + document.title);
}
});
}
var exportEl = document.getElementById("exportSettings");
if (exportEl) {
exportEl.addEventListener("click", function() {
try {
var settings = EducationHelper.Storage.get("settings", {});
var blob = new Blob([JSON.stringify(settings, null, 2)], { type: "application/json" });
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = "齐大教务助手_设置_" + (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + ".json";
a.click();
URL.revokeObjectURL(url);
EducationHelper.Logger.success("设置已导出");
EducationHelper.UI.showMessage("设置已导出为 JSON 文件", "success", 2e3);
} catch (error) {
EducationHelper.Logger.error("导出设置失败", error);
EducationHelper.UI.showMessage("导出失败: " + error.message, "error", 3e3);
}
});
}
var importBtn = document.getElementById("importSettings");
var importFile = document.getElementById("importSettingsFile");
if (importBtn && importFile) {
importBtn.addEventListener("click", function() {
importFile.click();
});
importFile.addEventListener("change", function(event) {
var file = event.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(e) {
try {
var imported = JSON.parse(e.target.result);
if (typeof imported !== "object" || imported === null) {
throw new Error("无效的设置文件格式");
}
EducationHelper.Storage.set("settings", imported);
EducationHelper.Config.loadSavedSettings();
EducationHelper.Status.syncFromState();
EducationHelper.Logger.success("设置已导入,部分设置需刷新页面生效");
EducationHelper.UI.showMessage("设置导入成功!部分设置需刷新页面", "success", 3e3);
} catch (error) {
EducationHelper.Logger.error("导入设置失败", error);
EducationHelper.UI.showMessage("导入失败: " + error.message, "error", 3e3);
}
};
reader.readAsText(file);
event.target.value = "";
});
}
var exportLogsEl = document.getElementById("exportLogs");
if (exportLogsEl) {
exportLogsEl.addEventListener("click", function() {
var debugContent = document.getElementById("debugContent");
var logText = debugContent ? debugContent.innerText : "无日志内容";
var header = "齐大教务助手 调试日志\n导出时间: " + (/* @__PURE__ */ new Date()).toLocaleString() + "\n页面URL: " + window.location.href + "\n版本: 2.7.1\n========================================\n\n";
var blob = new Blob([header + logText], { type: "text/plain;charset=utf-8" });
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = "齐大教务助手_日志_" + (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + ".txt";
a.click();
URL.revokeObjectURL(url);
EducationHelper.UI.showMessage("日志已导出", "success", 2e3);
});
}
},
// 创建UI界面
create: function() {
EducationHelper.Logger.action("正在创建助手面板...");
if (document.getElementById("qqhruHelperUI")) {
this.elements.container = document.getElementById("qqhruHelperUI");
this.elements.dragBar = document.getElementById("dragBar");
this.elements.content = document.getElementById("uiContent");
return this;
}
addStyles();
this.elements.container = document.createElement("div");
this.elements.container.id = "qqhruHelperUI";
this.elements.container.style.position = EducationHelper.Config.state.uiFollowPage ? "fixed" : "absolute";
this.elements.container.style.top = "20px";
this.elements.container.style.right = "20px";
this.elements.container.style.padding = "0";
this.elements.container.style.zIndex = "9999";
this.elements.container.style.overflow = "hidden";
if (EducationHelper.Config.state.panelPosition) {
this.elements.container.style.left = `${EducationHelper.Config.state.panelPosition.left}px`;
this.elements.container.style.top = `${EducationHelper.Config.state.panelPosition.top}px`;
this.elements.container.style.right = "auto";
}
this.elements.container.innerHTML = `
`;
document.body.appendChild(this.elements.container);
this.elements.dragBar = document.getElementById("dragBar");
this.elements.content = document.getElementById("uiContent");
this.syncViewportConstraints();
if (EducationHelper.Config.state.panelPosition) {
this.applyPanelPosition(
EducationHelper.Config.state.panelPosition.left,
EducationHelper.Config.state.panelPosition.top
);
}
this.generateContent();
this.makeDraggable();
document.getElementById("minimizeBtn").addEventListener("click", this.toggleMinimize.bind(this));
document.getElementById("closeBtn").addEventListener("click", () => {
EducationHelper.shutdown("用户关闭面板");
this.elements.container.style.transform = "scale(0.8)";
this.elements.container.style.opacity = "0";
EducationHelper.Runtime.setTimeout(() => {
this.elements.container.style.display = "none";
this.elements.container.style.transform = "scale(1)";
this.elements.container.style.opacity = "1";
}, 200);
});
EducationHelper.Logger.success("助手面板创建完成");
return this;
},
generateContent: function() {
EducationHelper.Logger.action("生成页面内容");
},
makeDraggable: function() {
let offsetX = 0;
let offsetY = 0;
let isDragging = false;
this.elements.dragBar.addEventListener("mousedown", (e) => {
isDragging = true;
this.elements.container.style.right = "auto";
offsetX = e.clientX - this.elements.container.getBoundingClientRect().left;
offsetY = e.clientY - this.elements.container.getBoundingClientRect().top;
this.elements.dragBar.style.cursor = "grabbing";
document.body.style.userSelect = "none";
});
document.addEventListener("mousemove", (e) => {
if (isDragging) {
const newX = e.clientX - offsetX;
const newY = e.clientY - offsetY;
this.applyPanelPosition(newX, newY);
}
});
document.addEventListener("mouseup", () => {
isDragging = false;
this.elements.dragBar.style.cursor = "grab";
document.body.style.userSelect = "";
const rect = this.elements.container.getBoundingClientRect();
EducationHelper.Config.state.panelPosition = this.clampPanelPosition(rect.left, rect.top);
EducationHelper.Config.saveSettings();
});
window.addEventListener("resize", () => {
if (!this.elements.container || this.elements.container.style.display === "none") return;
this.syncViewportConstraints();
const rect = this.elements.container.getBoundingClientRect();
const position = this.applyPanelPosition(rect.left, rect.top);
EducationHelper.Config.state.panelPosition = {
left: Math.round(position.left),
top: Math.round(position.top)
};
EducationHelper.Config.saveSettings();
});
},
toggleMinimize: function() {
const content = this.elements.content;
const btn = document.getElementById("minimizeBtn");
if (content.style.display === "none") {
content.style.display = "block";
this.syncViewportConstraints();
content.style.opacity = "0";
content.style.transform = "translateY(-10px)";
EducationHelper.Runtime.setTimeout(() => {
content.style.transition = "all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)";
content.style.opacity = "1";
content.style.transform = "translateY(0)";
}, 10);
btn.textContent = "−";
btn.style.transform = "rotate(0deg)";
} else {
content.style.transition = "all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)";
content.style.opacity = "0";
content.style.transform = "translateY(-10px)";
EducationHelper.Runtime.setTimeout(() => {
content.style.display = "none";
content.style.transition = "";
}, 300);
btn.textContent = "+";
btn.style.transform = "rotate(90deg)";
}
},
showMessage: function(message, type = "info", duration = 3e3) {
const msgElement = document.createElement("div");
msgElement.style = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.96);
padding: 14px 16px;
border-radius: 12px;
z-index: 10000;
text-align: center;
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12);
color: #172334;
font-weight: 600;
font-size: 14px;
line-height: 1.5;
opacity: 0;
transition: all 0.2s ease;
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
max-width: 280px;
min-width: 180px;
border: 1px solid #DBE3EC;
background: #FFFFFF;
`;
switch (type) {
case "success":
msgElement.style.background = "#F0FDF4";
msgElement.style.borderColor = "#BBF7D0";
msgElement.style.color = "#166534";
break;
case "error":
msgElement.style.background = "#FEF2F2";
msgElement.style.borderColor = "#FECACA";
msgElement.style.color = "#B91C1C";
break;
case "warning":
msgElement.style.background = "#FFF7ED";
msgElement.style.borderColor = "#FED7AA";
msgElement.style.color = "#C2410C";
break;
default:
msgElement.style.background = "#EFF6FF";
msgElement.style.borderColor = "#BFDBFE";
msgElement.style.color = "#1D4ED8";
}
msgElement.innerHTML = message;
document.body.appendChild(msgElement);
EducationHelper.Runtime.setTimeout(() => {
msgElement.style.opacity = "1";
msgElement.style.transform = "translate(-50%, -50%) scale(1)";
}, 10);
if (duration > 0) {
EducationHelper.Runtime.setTimeout(() => {
if (msgElement && document.contains(msgElement)) {
msgElement.style.opacity = "0";
msgElement.style.transform = "translate(-50%, -50%) scale(0.96)";
EducationHelper.Runtime.setTimeout(() => {
if (msgElement && document.contains(msgElement)) {
msgElement.remove();
}
}, 300);
}
}, duration);
}
return msgElement;
}
};
EducationHelper.CourseGrabber = {
// 课程列表操作
courseList: {
add: function(courseCode) {
if (!courseCode) return false;
const courses = courseCode.split("\n").map((code) => code.trim());
courses.forEach((course) => {
if (course && !EducationHelper.Config.state.targetCourses.includes(course)) {
EducationHelper.Config.state.targetCourses.push(course);
}
});
this.updateUI();
EducationHelper.Status.syncFromState();
EducationHelper.Logger.success(`已添加课程: ${courses.join(", ")}`);
return true;
},
remove: function(index) {
if (index >= 0 && index < EducationHelper.Config.state.targetCourses.length) {
const removedCourse = EducationHelper.Config.state.targetCourses.splice(index, 1)[0];
this.updateUI();
EducationHelper.Status.render();
EducationHelper.Status.syncFromState();
EducationHelper.Logger.success(`已移除课程: ${removedCourse}`);
return true;
}
return false;
},
updateUI: function() {
const courseListDiv = document.getElementById("courseList");
if (!courseListDiv) return;
courseListDiv.innerHTML = "";
if (EducationHelper.Config.state.targetCourses.length === 0) {
courseListDiv.innerHTML = `
暂无课程,请先添加课程代码
`;
} else {
EducationHelper.Config.state.targetCourses.forEach((course, index) => {
const courseItem = document.createElement("div");
courseItem.className = "course-item";
courseItem.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #E5E5EA;
font-size: 15px;
font-weight: 500;
`;
courseItem.innerHTML = `
${course}
删除
`;
if (index === EducationHelper.Config.state.targetCourses.length - 1) {
courseItem.style.borderBottom = "none";
}
courseListDiv.appendChild(courseItem);
});
courseListDiv.querySelectorAll(".delete-btn").forEach((btn) => {
btn.addEventListener("click", (e) => {
const idx = parseInt(e.target.getAttribute("data-index"));
this.remove(idx);
});
btn.addEventListener("mouseenter", function() {
this.style.background = "#FFE4E6";
});
btn.addEventListener("mouseleave", function() {
this.style.background = "#FFF1F2";
});
});
}
}
},
// 匹配成功课程操作
matchedCourses: {
add: function(courseCode) {
if (courseCode && !EducationHelper.Config.state.matchedCourses.includes(courseCode)) {
EducationHelper.Config.state.matchedCourses.push(courseCode);
this.updateUI();
EducationHelper.Logger.success(`已添加匹配成功课程: ${courseCode}`);
return true;
}
return false;
},
updateUI: function() {
const matchedCoursesDiv = document.getElementById("matchedCourses");
if (!matchedCoursesDiv) return;
matchedCoursesDiv.innerHTML = "";
if (EducationHelper.Config.state.matchedCourses.length === 0) {
matchedCoursesDiv.innerHTML = `
暂无匹配成功的课程
`;
} else {
EducationHelper.Config.state.matchedCourses.forEach((course, index) => {
const courseItem = document.createElement("div");
courseItem.className = "course-item";
courseItem.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #E5E5EA;
font-size: 15px;
font-weight: 500;
`;
courseItem.innerHTML = `
已匹配
${course}
`;
if (index === EducationHelper.Config.state.matchedCourses.length - 1) {
courseItem.style.borderBottom = "none";
}
matchedCoursesDiv.appendChild(courseItem);
});
}
}
},
// 抢课过程控制
control: {
getCourseRows: function(iframeDoc) {
return Array.from(iframeDoc.querySelectorAll(BrowserHelperCore.selectors.courseRows)).map((row) => {
const checkbox = row.querySelector(BrowserHelperCore.selectors.courseCheckbox);
if (!checkbox) {
return null;
}
const text = EducationHelper.Utils.getElementText(row);
const numberTokens = text.match(/\d+/g) || [];
return {
row,
checkbox,
text,
numberTokens
};
}).filter(Boolean);
},
matchCourseRow: function(courseRow, targetCourse) {
return BrowserHelperCore.matchCourseTarget(
targetCourse,
courseRow.text,
courseRow.numberTokens
);
},
start: function() {
if (EducationHelper.Config.state.targetCourses.length === 0) {
EducationHelper.UI.showMessage("请先添加课程", "error");
return false;
}
if (EducationHelper.Config.state.timer) {
EducationHelper.Logger.warn("抢课脚本已经在运行中");
return true;
}
document.getElementById("startScript").disabled = true;
document.getElementById("stopScript").disabled = false;
EducationHelper.Status.setNextAction("扫描课程列表并匹配目标课程");
if (EducationHelper.Config.state.dryRunMode) {
EducationHelper.Logger.action("演练模式下执行一次识别,不会进行实际勾选或提交");
this.checkAndSelectCourses();
document.getElementById("startScript").disabled = false;
document.getElementById("stopScript").disabled = true;
return true;
}
EducationHelper.Config.state.timer = EducationHelper.Runtime.setInterval(
this.checkAndSelectCourses.bind(this),
1e3
);
EducationHelper.Logger.action("抢课脚本已启动");
this.checkAndSelectCourses();
return true;
},
stop: function() {
if (EducationHelper.Config.state.timer) {
EducationHelper.Runtime.clearInterval(EducationHelper.Config.state.timer);
EducationHelper.Config.state.timer = null;
document.getElementById("startScript").disabled = false;
document.getElementById("stopScript").disabled = true;
EducationHelper.Status.setNextAction("等待用户重新开始");
EducationHelper.Logger.action("抢课脚本已停止");
return true;
}
return false;
},
checkAndSelectCourses: function() {
var _a;
EducationHelper.Logger.action("开始检查课程...");
const iframeDoc = (_a = document.querySelector("#ifra")) == null ? void 0 : _a.contentDocument;
if (!iframeDoc) {
EducationHelper.Logger.error("无法获取 iframe 文档");
return;
}
if (EducationHelper.Config.state.targetCourses.length === 0) {
EducationHelper.Logger.action("所有课程已处理,尝试提交...");
this.clickSubmitButton();
this.stop();
return;
}
const courseRows = this.getCourseRows(iframeDoc);
EducationHelper.Logger.debug(`本轮共找到 ${courseRows.length} 条可选课程记录`);
EducationHelper.Status.setRecognized(`识别到 ${courseRows.length} 条课程记录`);
const matchedCourses = [];
const unmatchedCourses = [];
const matchedTargets = [];
EducationHelper.Config.state.targetCourses.forEach((targetCourse) => {
const matchedRow = courseRows.find((courseRow) => this.matchCourseRow(courseRow, targetCourse));
if (!matchedRow) {
unmatchedCourses.push(targetCourse);
return;
}
matchedTargets.push(matchedRow.checkbox);
if (EducationHelper.Config.state.dryRunMode) {
matchedCourses.push(targetCourse);
return;
}
if (!matchedRow.checkbox.checked) {
EducationHelper.Utils.clickElement(matchedRow.checkbox);
EducationHelper.Logger.success(`已勾选课程: ${targetCourse}`);
}
matchedCourses.push(targetCourse);
EducationHelper.CourseGrabber.matchedCourses.add(targetCourse);
});
if (EducationHelper.Config.state.dryRunMode) {
if (matchedTargets.length > 0) {
EducationHelper.Utils.handleDryRun(
matchedTargets,
`已识别 ${matchedTargets.length} 个可勾选课程,演练模式下不会真正勾选`
);
EducationHelper.Status.setRecognized(`演练命中 ${matchedCourses.length} / 目标 ${EducationHelper.Config.state.targetCourses.length}`);
} else {
EducationHelper.Status.setRecognized(`演练未命中,目标 ${EducationHelper.Config.state.targetCourses.length}`);
}
return;
}
EducationHelper.Config.state.targetCourses = unmatchedCourses;
EducationHelper.CourseGrabber.courseList.updateUI();
EducationHelper.Status.syncFromState();
matchedCourses.forEach((course) => {
EducationHelper.Logger.action(`本轮已处理课程: ${course}`);
});
if (matchedCourses.length === 0) {
EducationHelper.Logger.warn(`本轮未匹配到目标课程: ${EducationHelper.Config.state.targetCourses.join(", ")}`);
}
if (EducationHelper.Config.state.targetCourses.length === 0 && matchedCourses.length > 0) {
EducationHelper.Logger.action("目标课程已全部处理,尝试提交...");
this.clickSubmitButton();
this.stop();
}
},
clickSubmitButton: function() {
const button = document.querySelector("#submitButton");
if (button) {
EducationHelper.Logger.action("找到提交按钮,正在尝试提交...");
if (EducationHelper.Utils.handleDryRun(button, "已识别提交按钮,演练模式下不会真正提交选课")) {
return true;
}
EducationHelper.Utils.clickElement(button);
EducationHelper.Logger.success("已提交选课请求");
return true;
} else {
EducationHelper.Logger.warn("未找到提交按钮,请检查页面结构");
return false;
}
}
},
// 选课监控模块
monitor: {
roundCount: 0,
start: function() {
if (EducationHelper.Config.state.courseMonitorTimer) {
EducationHelper.Logger.warn("监控已在运行中");
return;
}
if (EducationHelper.Config.state.targetCourses.length === 0) {
EducationHelper.UI.showMessage("请先添加要监控的课程代码", "error", 2e3);
return;
}
const interval = Math.max(3, EducationHelper.Config.state.courseMonitorInterval) * 1e3;
this.roundCount = 0;
EducationHelper.Config.state.courseMonitorEnabled = true;
EducationHelper.Logger.action(`选课监控已启动,每 ${interval / 1e3} 秒刷新一次`);
this.updateStatusUI(true);
this.checkOnce();
EducationHelper.Config.state.courseMonitorTimer = EducationHelper.Runtime.setInterval(
() => this.checkOnce(),
interval
);
},
stop: function() {
EducationHelper.Runtime.clearInterval(EducationHelper.Config.state.courseMonitorTimer);
EducationHelper.Config.state.courseMonitorTimer = null;
EducationHelper.Config.state.courseMonitorEnabled = false;
EducationHelper.Logger.action("选课监控已停止");
this.updateStatusUI(false);
},
checkOnce: function() {
var _a;
this.roundCount++;
const iframeDoc = (_a = document.querySelector("#ifra")) == null ? void 0 : _a.contentDocument;
if (!iframeDoc) {
const iframe = document.querySelector("#ifra");
if (iframe) {
EducationHelper.Logger.debug(`监控第 ${this.roundCount} 轮:刷新 iframe`);
try {
iframe.contentWindow.location.reload();
} catch (e) {
iframe.src = iframe.src;
}
}
this.updateMonitorStatusText(`第 ${this.roundCount} 轮 · 等待 iframe 加载...`);
return;
}
const courseRows = EducationHelper.CourseGrabber.control.getCourseRows(iframeDoc);
const targets = EducationHelper.Config.state.targetCourses;
let foundAvailable = [];
targets.forEach((target) => {
const matched = courseRows.find(
(row) => EducationHelper.CourseGrabber.control.matchCourseRow(row, target)
);
if (matched) {
foundAvailable.push({ target, row: matched });
}
});
if (foundAvailable.length > 0) {
const names = foundAvailable.map((f) => f.target).join("、");
EducationHelper.Logger.success(`监控发现可选课程: ${names}`);
EducationHelper.Notification.courseGrabbed(names);
EducationHelper.UI.showMessage(`发现可选课程:${names}`, "success", 5e3);
if (!EducationHelper.Config.state.dryRunMode) {
foundAvailable.forEach(({ row }) => {
if (!row.checkbox.checked) {
EducationHelper.Utils.clickElement(row.checkbox);
}
});
EducationHelper.Logger.action("已自动勾选发现的可选课程");
}
this.stop();
return;
}
this.updateMonitorStatusText(`第 ${this.roundCount} 轮 · 未发现余量 · 继续监控中...`);
EducationHelper.Logger.debug(`监控第 ${this.roundCount} 轮:未发现目标课程余量`);
try {
const iframe = document.querySelector("#ifra");
if (iframe) {
iframe.contentWindow.location.reload();
}
} catch (e) {
EducationHelper.Logger.debug("刷新 iframe 失败", e);
}
},
updateStatusUI: function(running) {
const startBtn = document.getElementById("startMonitor");
const stopBtn = document.getElementById("stopMonitor");
const statusDiv = document.getElementById("monitorStatus");
if (startBtn) startBtn.disabled = running;
if (stopBtn) stopBtn.disabled = !running;
if (statusDiv) statusDiv.style.display = running ? "block" : "none";
},
updateMonitorStatusText: function(text) {
const statusDiv = document.getElementById("monitorStatus");
if (statusDiv) statusDiv.textContent = text;
}
},
// UI内容生成
generateUI: function() {
return `
${EducationHelper.Status.generatePanel()}
演练模式(只识别,不提交)
操作提示
演练模式只会高亮识别结果。关闭后才会真正勾选并提交课程。
课程代码
添加课程
开始抢课
停止
选课监控(自动刷新)
开启后定时刷新选课列表,发现目标课程有余量时自动勾选并通知。
刷新间隔
秒
开始监控
停止监控
监控中...
`;
},
bindEvents: function() {
document.getElementById("courseDryRunMode").addEventListener("change", function() {
EducationHelper.Config.state.dryRunMode = this.checked;
EducationHelper.Config.saveSettings();
EducationHelper.Status.syncFromState();
EducationHelper.Logger.action(`抢课演练模式: ${this.checked ? "已启用" : "已关闭"}`);
});
document.getElementById("addCourses").addEventListener("click", () => {
const courseCodesInput = document.getElementById("courseCode").value.trim();
if (this.courseList.add(courseCodesInput)) {
document.getElementById("courseCode").value = "";
}
});
document.getElementById("startScript").addEventListener("click", () => {
this.control.start();
});
document.getElementById("stopScript").addEventListener("click", () => {
this.control.stop();
});
document.getElementById("monitorInterval").addEventListener("change", function() {
const val = Math.max(3, Math.min(60, parseInt(this.value) || 5));
this.value = val;
EducationHelper.Config.state.courseMonitorInterval = val;
EducationHelper.Logger.action(`监控刷新间隔已设为 ${val} 秒`);
});
document.getElementById("startMonitor").addEventListener("click", () => {
this.monitor.start();
});
document.getElementById("stopMonitor").addEventListener("click", () => {
this.monitor.stop();
});
},
init: function() {
if (!EducationHelper.Config.pageType.isCoursePage) return;
EducationHelper.Logger.action("初始化抢课模块...");
EducationHelper.Status.setNextAction("等待添加课程代码或开始识别");
EducationHelper.Status.syncFromState();
return this;
}
};
EducationHelper.Evaluator = {
// 选项选择功能
optionSelector: {
getOptionCandidates: function() {
return Array.from(document.querySelectorAll(BrowserHelperCore.selectors.evaluationChoices)).map((element) => {
var _a, _b;
const parentText = EducationHelper.Utils.getElementText(
element.closest("label, td, tr, li, div")
);
const siblingText = EducationHelper.Utils.normalizeText(
[
((_a = element.nextElementSibling) == null ? void 0 : _a.textContent) || "",
((_b = element.parentElement) == null ? void 0 : _b.textContent) || ""
].join(" ")
);
const text = EducationHelper.Utils.normalizeText(`${parentText} ${siblingText}`);
return {
element,
text
};
});
},
selectByLetter: function(letter) {
EducationHelper.Logger.action(`正在选择${letter}选项...`);
EducationHelper.Config.state.selectedOption = letter;
try {
const normalizedLetter = letter.toUpperCase();
const candidates = this.getOptionCandidates();
const matchedCandidates = candidates.filter(
(candidate) => candidate.text.includes(`(${normalizedLetter})`) || candidate.text.includes(`${normalizedLetter} `)
);
let selectedCount = 0;
if (matchedCandidates.length > 0 && EducationHelper.Utils.handleDryRun(
matchedCandidates.map((candidate) => candidate.element),
`已识别 ${matchedCandidates.length} 个 ${letter} 选项,演练模式下不会真正勾选`
)) {
EducationHelper.Status.setRecognized(`可选 ${letter} 项 ${matchedCandidates.length} 个`);
return true;
}
matchedCandidates.forEach((candidate) => {
if (EducationHelper.Utils.clickElement(candidate.element)) {
selectedCount++;
}
});
if (selectedCount === 0 && typeof $ === "function") {
$(".ace").each(function() {
const self = $(this);
const text = self.next().next().html();
if (text && text.indexOf(`(${normalizedLetter})`) !== -1) {
self.click();
selectedCount += 1;
}
});
}
if (selectedCount > 0) {
EducationHelper.Status.setRecognized(`已选择 ${selectedCount} 个${letter}选项`);
EducationHelper.Logger.success(`已选择 ${selectedCount} 个${letter}选项`);
return true;
}
EducationHelper.Logger.warn(`未找到可用的${letter}选项`);
EducationHelper.Status.setRecognized(`未找到 ${letter} 选项`);
return false;
} catch (error) {
EducationHelper.Logger.error(`选择${letter}选项时出错`, error);
return false;
}
}
},
// 评价内容填写
contentFiller: {
getComment: function() {
if (EducationHelper.Config.content.useRandomTemplate) {
const templates = EducationHelper.Config.content.evaluationTemplates;
if (templates && templates.length > 0) {
const picked = templates[Math.floor(Math.random() * templates.length)];
EducationHelper.Logger.debug(`随机选取评语模板: ${picked.substring(0, 20)}...`);
return picked;
}
}
return EducationHelper.Config.content.evaluationComment;
},
fillContent: function(content) {
content = content || this.getComment();
EducationHelper.Logger.action("正在填写评价内容...");
try {
let filled = false;
const mainTextarea = document.querySelector('textarea[name="zgpj"]');
if (mainTextarea) {
if (EducationHelper.Utils.handleDryRun(mainTextarea, "已识别主观评价文本框,演练模式下不会真正填写")) {
EducationHelper.Status.setRecognized("已识别主观评价文本框");
return true;
}
mainTextarea.value = content;
const event = new Event("input", { bubbles: true });
mainTextarea.dispatchEvent(event);
EducationHelper.Status.setRecognized("已填写主观评价文本框");
EducationHelper.Logger.success("已通过name='zgpj'找到并填写主观评价文本框");
filled = true;
}
if (!filled) {
const textareas = Array.from(document.querySelectorAll("textarea.form-control, textarea")).filter((textarea) => EducationHelper.Utils.isElementVisible(textarea));
if (textareas.length > 0) {
if (EducationHelper.Utils.handleDryRun(textareas, `已识别 ${textareas.length} 个文本框,演练模式下不会真正填写`)) {
EducationHelper.Status.setRecognized(`文本框 ${textareas.length} 个`);
return true;
}
for (const textarea of textareas) {
textarea.value = content;
const event = new Event("input", { bubbles: true });
textarea.dispatchEvent(event);
}
EducationHelper.Status.setRecognized(`已填写 ${textareas.length} 个文本框`);
EducationHelper.Logger.success("已填写所有文本框");
filled = true;
}
}
if (!filled && typeof $ === "function") {
const jqTextarea = $(EducationHelper.Config.selectors.evalJqTextarea);
if (jqTextarea.length > 0) {
jqTextarea.val(content);
EducationHelper.Status.setRecognized("已填写主观评价文本框");
EducationHelper.Logger.success("已使用jQuery选择器填写评价内容");
filled = true;
}
}
if (!filled) {
const allTextareas = document.querySelectorAll("textarea");
if (allTextareas.length > 0) {
for (const textarea of allTextareas) {
textarea.value = content;
const event = new Event("input", { bubbles: true });
textarea.dispatchEvent(event);
}
EducationHelper.Status.setRecognized(`已填写 ${allTextareas.length} 个文本框`);
EducationHelper.Logger.success("已填写所有找到的文本区域");
filled = true;
}
}
return filled;
} catch (error) {
EducationHelper.Logger.error("填写评价内容时出错", error);
return false;
}
}
},
// 评价提交处理
submitter: {
countdown: {
timer: null,
seconds: 0,
start: function(duration, onComplete) {
this.stop();
this.seconds = duration || EducationHelper.Config.timers.autoSubmitDelay / 1e3;
this.ensureDisplayExists();
this.updateDisplay();
this.timer = EducationHelper.Runtime.setInterval(() => {
this.seconds--;
this.updateDisplay();
if (this.seconds <= 0) {
this.stop();
if (typeof onComplete === "function") {
onComplete();
}
}
}, 1e3);
return this;
},
stop: function() {
if (this.timer) {
EducationHelper.Runtime.clearInterval(this.timer);
this.timer = null;
}
return this;
},
ensureDisplayExists: function() {
let timerDisplay = document.getElementById("timerDisplay");
if (!timerDisplay) {
EducationHelper.Logger.action("创建倒计时显示元素");
timerDisplay = document.createElement("div");
timerDisplay.id = "timerDisplay";
timerDisplay.className = "timer-display";
const uiContent = document.getElementById("uiContent");
if (uiContent) {
const buttons = uiContent.querySelector('div[style*="display: flex"]');
if (buttons) {
uiContent.insertBefore(timerDisplay, buttons);
} else {
uiContent.appendChild(timerDisplay);
}
} else {
document.body.appendChild(timerDisplay);
}
}
timerDisplay.style.display = "block";
timerDisplay.style.backgroundColor = "#e3f2fd";
timerDisplay.style.border = "1px solid #1976d2";
timerDisplay.style.padding = "10px";
timerDisplay.style.borderRadius = "4px";
timerDisplay.style.marginBottom = "10px";
timerDisplay.style.fontWeight = "bold";
timerDisplay.style.fontSize = "16px";
timerDisplay.style.textAlign = "center";
timerDisplay.style.boxShadow = "0 2px 5px rgba(0,0,0,0.2)";
if (!timerDisplay.innerHTML || !timerDisplay.innerHTML.includes("timerValue")) {
timerDisplay.innerHTML = '等待提交: 120 秒';
}
if (EducationHelper.Config.state.autoMode) {
timerDisplay.style.backgroundColor = "#e8f5e9";
timerDisplay.style.border = "1px solid #4caf50";
timerDisplay.innerHTML = '自动提交倒计时: 120 秒 ';
}
return timerDisplay;
},
updateDisplay: function() {
const timerElement = document.getElementById("timerValue");
if (timerElement) {
timerElement.textContent = this.seconds;
}
return this;
}
},
findSubmitButton: function() {
var _a;
const candidates = Array.from(document.querySelectorAll(BrowserHelperCore.selectors.submitAction)).filter((element) => EducationHelper.Utils.isElementVisible(element)).map((element) => {
const text = EducationHelper.Utils.getElementText(element);
const rect = element.getBoundingClientRect();
return {
element,
score: BrowserHelperCore.scoreSubmitCandidate({
text,
type: element.type,
className: element.className,
rectTop: rect.top,
viewportHeight: window.innerHeight
})
};
}).filter((item) => item.score > 0).sort((a, b) => b.score - a.score);
return ((_a = candidates[0]) == null ? void 0 : _a.element) || null;
},
findConfirmButton: function() {
const exactMatch = EducationHelper.Utils.findClickableByText(
["确定", "确认", "是"],
BrowserHelperCore.selectors.confirmAction
);
return exactMatch;
},
findAndClickSubmitButton: async function(showMessage = true) {
EducationHelper.Logger.action("查找提交按钮...");
let statusMsg = null;
if (showMessage) {
statusMsg = EducationHelper.UI.showMessage("正在提交评价...
", "info", 0);
}
const submitButton = await EducationHelper.Utils.waitFor(
() => this.findSubmitButton() || document.querySelector('#submit, #btnSubmit, .submit-btn, [name="submit"]'),
{ timeout: 4e3, message: "未找到提交按钮" }
).catch(() => null);
if (submitButton) {
if (statusMsg) {
statusMsg.innerHTML = "找到提交按钮,正在点击...
";
}
if (EducationHelper.Utils.handleDryRun(submitButton, "已识别提交按钮,演练模式下不会真正提交评价")) {
if (statusMsg && document.contains(statusMsg)) {
statusMsg.remove();
}
return true;
}
EducationHelper.Logger.action("找到提交按钮,点击提交");
EducationHelper.Utils.clickElement(submitButton);
await this.handleConfirmDialog(statusMsg);
return true;
}
EducationHelper.Logger.warn("未找到提交按钮");
if (statusMsg) {
statusMsg.innerHTML = '未找到提交按钮,尝试直接提交表单...
';
}
const mainForm = document.querySelector("form");
if (mainForm) {
EducationHelper.Logger.action("尝试直接提交表单");
try {
mainForm.submit();
EducationHelper.Logger.success("已调用表单的submit()方法");
if (statusMsg) {
statusMsg.innerHTML = '已尝试提交表单,请检查是否成功
';
EducationHelper.Runtime.setTimeout(() => {
if (statusMsg && document.contains(statusMsg)) {
statusMsg.remove();
}
}, 2e3);
}
return true;
} catch (error) {
EducationHelper.Logger.error("尝试提交表单失败", error);
}
}
if (statusMsg) {
statusMsg.innerHTML = '自动提交失败,请手动点击页面中的"提交"按钮
';
EducationHelper.Runtime.setTimeout(() => {
if (statusMsg && document.contains(statusMsg)) {
statusMsg.remove();
}
}, 5e3);
}
return false;
},
handleConfirmDialog: async function(statusMsg) {
EducationHelper.Logger.action("等待确认对话框...");
const confirmButton = await EducationHelper.Utils.waitFor(
() => this.findConfirmButton(),
{ timeout: 3e3, message: "未出现确认按钮" }
).catch(() => null);
if (!confirmButton) {
EducationHelper.Logger.action("未找到确认按钮,可能评价已直接提交或需要手动确认");
if (statusMsg) {
statusMsg.innerHTML = '可能需要手动确认提交,请检查是否有弹出确认窗口
';
EducationHelper.Runtime.setTimeout(() => {
if (statusMsg && document.contains(statusMsg)) {
statusMsg.remove();
}
}, 5e3);
}
return false;
}
EducationHelper.Logger.action("找到确认按钮,确认提交");
if (statusMsg) {
statusMsg.innerHTML = "找到确认按钮,正在确认提交...
";
}
EducationHelper.Utils.clickElement(confirmButton);
if (statusMsg) {
statusMsg.innerHTML = '评价提交成功!
';
EducationHelper.Runtime.setTimeout(() => {
if (statusMsg && document.contains(statusMsg)) {
statusMsg.remove();
}
}, 2e3);
}
EducationHelper.Logger.success("已完成评价提交");
EducationHelper.UI.showMessage(`
评价提交成功
可以继续处理下一个评估项。
`, "success", 2e3);
if (EducationHelper.Config.state.autoMode) {
EducationHelper.Runtime.setTimeout(() => {
const fallbackCandidates = Array.from(document.querySelectorAll(BrowserHelperCore.selectors.backAction)).filter((element) => EducationHelper.Utils.isElementVisible(element)).map((element) => ({
element,
text: EducationHelper.Utils.getElementText(element),
href: element.getAttribute("href") || ""
}));
const bestBackCandidate = BrowserHelperCore.pickBestBackCandidate(fallbackCandidates);
const backBtn = EducationHelper.Utils.findClickableByText(["返回", "列表", "评估列表"]) || (bestBackCandidate == null ? void 0 : bestBackCandidate.element);
if (backBtn) {
EducationHelper.Logger.action("找到返回按钮,自动返回列表页");
EducationHelper.Utils.clickElement(backBtn);
} else {
EducationHelper.Logger.warn("未找到返回列表入口,尝试 history.back()");
window.history.back();
}
}, 1500);
}
return true;
}
},
// 流程控制
process: {
start: async function() {
var _a;
EducationHelper.Logger.action("开始评价流程...");
EducationHelper.Status.setNextAction("识别题项、填写评价内容");
await EducationHelper.Utils.waitFor(
() => document.querySelector(BrowserHelperCore.selectors.evaluationInputs),
{ timeout: 1e4, message: "评价页面元素未加载完成" }
).catch((error) => {
EducationHelper.Logger.warn(error.message);
return null;
});
EducationHelper.UI.showMessage(
"正在进行评价操作...
",
"info",
2e3
);
EducationHelper.Evaluator.optionSelector.selectByLetter(
EducationHelper.Config.state.selectedOption
);
if (EducationHelper.Config.state.dryRunMode) {
EducationHelper.Evaluator.contentFiller.fillContent(
((_a = document.getElementById("evaluationContent")) == null ? void 0 : _a.value) || EducationHelper.Config.content.evaluationComment
);
EducationHelper.Status.setNextAction("关闭演练模式后可执行真实评价流程");
return;
}
EducationHelper.Runtime.setTimeout(() => {
var _a2;
EducationHelper.Evaluator.contentFiller.fillContent(
((_a2 = document.getElementById("evaluationContent")) == null ? void 0 : _a2.value) || EducationHelper.Config.content.evaluationComment
);
const startButton = document.getElementById("startEvaluation");
if (startButton) {
startButton.disabled = true;
startButton.style.opacity = "0.6";
startButton.textContent = "评价已开始";
}
if (EducationHelper.Config.state.autoSubmitEnabled) {
EducationHelper.Logger.action("已启用自动提交,开始倒计时...");
EducationHelper.Status.setNextAction("等待 120 秒后自动提交");
EducationHelper.Evaluator.submitter.countdown.start(
EducationHelper.Config.timers.autoSubmitDelay / 1e3,
() => {
if (EducationHelper.Config.state.autoSubmitEnabled) {
EducationHelper.Logger.action("倒计时结束,自动提交评价");
EducationHelper.UI.showMessage(
`倒计时结束
正在自动提交评价...
`,
"info",
2e3
);
EducationHelper.Runtime.setTimeout(() => {
EducationHelper.Evaluator.process.submit();
}, 2e3);
}
}
);
}
}, 500);
},
submit: async function() {
EducationHelper.Logger.action("提交评价...");
EducationHelper.Status.setNextAction("识别提交按钮并尝试提交");
const contentElement = document.getElementById("evaluationContent");
if (contentElement) {
EducationHelper.Config.content.evaluationComment = contentElement.value;
EducationHelper.Config.saveSettings();
}
EducationHelper.Evaluator.contentFiller.fillContent();
await EducationHelper.Evaluator.submitter.findAndClickSubmitButton();
}
},
generateUI: function() {
return `
${EducationHelper.Status.generatePanel()}
演练模式(只识别,不提交)
等待说明
教务系统要求等待 120 秒,脚本不会跳过,只会自动填写并到点提交。
选择选项
立即提交
开始流程
等待提交: 120 秒
`;
},
bindEvents: function() {
document.getElementById("evaluationDryRunMode").addEventListener("change", function() {
EducationHelper.Config.state.dryRunMode = this.checked;
EducationHelper.Config.saveSettings();
EducationHelper.Status.syncFromState();
EducationHelper.Logger.action(`评估演练模式: ${this.checked ? "已启用" : "已关闭"}`);
});
const optionRadios = Array.from(document.getElementsByName("evaluationOption"));
optionRadios.forEach((radio) => {
radio.addEventListener("change", function() {
EducationHelper.Config.state.selectedOption = this.value;
EducationHelper.Logger.action(`已选择${this.value}选项`);
EducationHelper.Config.saveSettings();
});
});
document.getElementById("useRandomTemplate").addEventListener("change", function() {
EducationHelper.Config.content.useRandomTemplate = this.checked;
EducationHelper.Config.saveSettings();
EducationHelper.Logger.action(`随机评语模板: ${this.checked ? "已启用" : "已禁用"}`);
const previewBtn2 = document.getElementById("previewRandomTemplate");
if (previewBtn2) {
previewBtn2.style.display = this.checked ? "block" : "none";
}
});
document.getElementById("previewRandomTemplate").addEventListener("click", function() {
const templates = EducationHelper.Config.content.evaluationTemplates;
if (templates && templates.length > 0) {
const picked = templates[Math.floor(Math.random() * templates.length)];
const textarea = document.getElementById("evaluationContent");
if (textarea) {
textarea.value = picked;
}
EducationHelper.UI.showMessage(`预览评语:${picked.substring(0, 30)}...`, "info", 2e3);
}
});
const previewBtn = document.getElementById("previewRandomTemplate");
if (previewBtn) {
previewBtn.style.display = EducationHelper.Config.content.useRandomTemplate ? "block" : "none";
}
document.getElementById("autoSubmitEvaluation").addEventListener("change", function() {
EducationHelper.Config.state.autoSubmitEnabled = this.checked;
EducationHelper.Logger.action(`自动提交评价: ${this.checked ? "已启用" : "已禁用"}`);
EducationHelper.Status.setNextAction(this.checked ? "等待 120 秒后自动提交" : "等待用户手动提交");
EducationHelper.Config.saveSettings();
});
document.getElementById("evaluationContent").addEventListener("input", function() {
EducationHelper.Config.content.evaluationComment = this.value;
EducationHelper.Status.setRecognized(`题项 ${document.querySelectorAll(BrowserHelperCore.selectors.evaluationChoices).length} / 文本框 ${document.querySelectorAll("textarea").length}`);
});
document.getElementById("selectOptions").addEventListener("click", () => {
EducationHelper.Evaluator.optionSelector.selectByLetter(
EducationHelper.Config.state.selectedOption
);
if (EducationHelper.Config.state.autoSubmitEnabled) {
EducationHelper.Logger.action("已启用自动提交,将在3秒后提交评价...");
EducationHelper.Status.setNextAction(EducationHelper.Config.state.dryRunMode ? "演练模式下仅预览提交按钮" : "即将尝试提交评价");
EducationHelper.Runtime.setTimeout(() => {
EducationHelper.Evaluator.process.submit();
}, 3e3);
}
});
document.getElementById("submitEvaluation").addEventListener("click", () => {
EducationHelper.Evaluator.process.submit();
});
document.getElementById("startEvaluation").addEventListener("click", () => {
EducationHelper.Evaluator.process.start();
});
},
init: function() {
if (!EducationHelper.Config.pageType.isEvaluationPage) return;
EducationHelper.Logger.action("初始化评估模块...");
EducationHelper.Status.setNextAction("等待开始评价流程");
EducationHelper.Status.syncFromState();
if (EducationHelper.Config.state.autoMode) {
EducationHelper.Logger.action("检测到全自动模式,自动开始评价流程");
EducationHelper.Runtime.setTimeout(() => {
this.process.start();
}, 1e3);
}
return this;
}
};
EducationHelper.EvaluationList = {
itemHelpers: {
getActionElements: function() {
const root = EducationHelper.Utils.getEvaluationSearchRoot();
return Array.from(root.querySelectorAll(BrowserHelperCore.selectors.evaluationAction)).filter((element) => EducationHelper.Utils.isElementVisible(element)).filter((element) => !EducationHelper.Utils.isIgnoredEvaluationElement(element));
},
isEvaluationAction: function(element) {
if (EducationHelper.Utils.isIgnoredEvaluationElement(element)) return false;
const text = EducationHelper.Utils.getElementText(element);
if (BrowserHelperCore.isEvaluationActionText(text)) return true;
if (EducationHelper.Config.pageType.isEvaluationListPage) {
const normalized = BrowserHelperCore.normalizeText(text);
if (normalized === "查看" && element.closest("tbody")) return true;
}
return false;
},
getContext: function(element) {
const context = element.closest(BrowserHelperCore.selectors.evaluationContainer) || element;
return EducationHelper.Utils.isIgnoredEvaluationElement(context) ? null : context;
},
isCompletedContext: function(context) {
const text = EducationHelper.Utils.getElementText(context);
if (BrowserHelperCore.isCompletedText(text)) return true;
if (context.querySelector) {
const successLabel = context.querySelector(".label-success");
if (successLabel) {
const labelText = BrowserHelperCore.normalizeText(successLabel.textContent);
if (labelText === "是" || labelText.includes("已评")) return true;
}
}
return false;
},
getEvaluationEntries: function() {
const root = EducationHelper.Utils.getEvaluationSearchRoot();
if (EducationHelper.Utils.hasEvaluationClosedNotice(root)) {
return [];
}
const entries = /* @__PURE__ */ new Map();
this.getActionElements().filter((element) => this.isEvaluationAction(element)).forEach((element) => {
const context = this.getContext(element);
if (!context) {
return;
}
if (!entries.has(context)) {
entries.set(context, {
context,
actions: []
});
}
entries.get(context).actions.push(element);
});
const normalizedEntries = Array.from(entries.values()).map((entry) => {
const enabledActions = entry.actions.filter(
(action) => !EducationHelper.Utils.isElementDisabled(action)
);
return {
context: entry.context,
actions: entry.actions,
enabledActions,
isCompleted: this.isCompletedContext(entry.context) || enabledActions.length === 0
};
});
if (normalizedEntries.length > 0) {
return normalizedEntries;
}
return Array.from(root.querySelectorAll("tr, .list-item, .evaluation-item")).filter((context) => EducationHelper.Utils.isElementVisible(context)).filter((context) => !EducationHelper.Utils.isIgnoredEvaluationElement(context)).filter((context) => !context.closest("thead")).map((context) => {
const text = EducationHelper.Utils.getElementText(context);
if (!text || !text.includes("评估") && !text.includes("评价") && !text.includes("教师") && !text.includes("问卷")) {
return null;
}
return {
context,
actions: [],
enabledActions: [],
isCompleted: this.isCompletedContext(context)
};
}).filter(Boolean);
},
getPendingActions: function() {
return this.getEvaluationEntries().filter((entry) => !entry.isCompleted).flatMap((entry) => entry.enabledActions).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
}
},
// 进度统计
progress: {
total: 0,
completed: 0,
observer: null,
updateTimer: null,
scheduleUpdate: function() {
EducationHelper.Runtime.clearTimeout(this.updateTimer);
this.updateTimer = EducationHelper.Runtime.setTimeout(() => {
this.updateProgress();
}, 150);
},
startTracking: function() {
this.stopTracking();
this.observer = EducationHelper.Runtime.observe(
document.body || document.documentElement,
{ childList: true, subtree: true, attributes: true },
() => this.scheduleUpdate()
);
},
stopTracking: function() {
EducationHelper.Runtime.disconnectObserver(this.observer);
this.observer = null;
EducationHelper.Runtime.clearTimeout(this.updateTimer);
this.updateTimer = null;
},
updateProgress: function() {
try {
const entries = EducationHelper.EvaluationList.itemHelpers.getEvaluationEntries();
const totalCount = entries.length;
const completedCount = entries.filter((entry) => entry.isCompleted).length;
const prevCompleted = this.completed;
this.total = totalCount;
this.completed = completedCount;
EducationHelper.Logger.debug(`进度统计: ${completedCount}/${totalCount}`);
if (totalCount > 0 && completedCount === totalCount && prevCompleted < totalCount) {
EducationHelper.Notification.evaluationComplete(completedCount, totalCount);
}
this.updateUI();
return { total: totalCount, completed: completedCount };
} catch (error) {
EducationHelper.Logger.error("统计评价进度时出错", error);
return { total: 0, completed: 0 };
}
},
updateUI: function() {
const progressDiv = document.getElementById("evaluationProgress");
if (!progressDiv) return;
const percentage = this.total > 0 ? Math.round(this.completed / this.total * 100) : 0;
const isCompleted = this.completed === this.total && this.total > 0;
const summaryText = this.total === 0 ? "当前页面没有待评项目" : isCompleted ? "所有评价已完成" : `完成度 ${percentage}% · 还剩 ${this.total - this.completed} 个`;
progressDiv.innerHTML = `
评估进度
${this.completed}/${this.total}
${summaryText}
`;
}
},
// 自动点击功能
autoClicker: {
timerId: null,
scanDelayTimerId: null,
stopCountdown: function() {
EducationHelper.Runtime.clearInterval(this.timerId);
EducationHelper.Runtime.clearTimeout(this.scanDelayTimerId);
this.timerId = null;
this.scanDelayTimerId = null;
},
startCountdown: function() {
EducationHelper.Logger.action(`开始自动点击倒计时,${EducationHelper.Config.timers.autoClickEvaluationDelay / 1e3}秒后开始执行...`);
this.stopCountdown();
const countdownElement = document.getElementById("countdownValue");
if (!countdownElement) return false;
let secondsLeft = EducationHelper.Config.timers.autoClickEvaluationDelay / 1e3;
countdownElement.textContent = secondsLeft;
EducationHelper.Config.state.autoClickStopped = false;
EducationHelper.Status.setNextAction(
EducationHelper.Config.state.dryRunMode ? "演练模式下将预览自动评估入口" : "等待倒计时后自动点击评估入口"
);
this.timerId = EducationHelper.Runtime.setInterval(() => {
if (EducationHelper.Config.state.autoClickStopped) {
this.stopCountdown();
EducationHelper.Logger.action("倒计时已被手动停止");
return;
}
secondsLeft -= 1;
countdownElement.textContent = secondsLeft;
if (secondsLeft <= 0) {
this.stopCountdown();
if ((EducationHelper.Config.state.autoClickEvaluationEnabled || EducationHelper.Config.state.autoMode) && !EducationHelper.Config.state.autoClickStopped) {
EducationHelper.Logger.action("倒计时结束,开始扫描评估按钮...");
const countdownDiv = document.getElementById("countdownDiv");
if (countdownDiv) {
countdownDiv.innerHTML = '自动操作开始执行... ';
}
this.scanDelayTimerId = EducationHelper.Runtime.setTimeout(() => {
if (!EducationHelper.Config.state.autoClickStopped) {
EducationHelper.EvaluationList.scanner.scanAndClick();
EducationHelper.Runtime.setTimeout(() => {
if (countdownDiv) {
countdownDiv.style.display = "none";
}
if (!EducationHelper.Config.state.autoMode) {
EducationHelper.Config.state.autoClickEvaluationEnabled = false;
const checkbox = document.getElementById("autoClickEvaluation");
if (checkbox) checkbox.checked = false;
}
}, 2e3);
} else {
EducationHelper.Logger.action("在延迟期间检测到停止请求,取消自动操作");
if (countdownDiv) {
countdownDiv.style.display = "none";
}
}
}, 500);
} else {
EducationHelper.Logger.action("倒计时结束,但自动点击已被禁用");
const countdownDiv = document.getElementById("countdownDiv");
if (countdownDiv) {
countdownDiv.style.display = "none";
}
}
}
}, 1e3);
return true;
}
},
// 扫描与点击
scanner: {
countdownTimerId: null,
delayedClickTimerId: null,
stopPendingClick: function() {
EducationHelper.Runtime.clearInterval(this.countdownTimerId);
EducationHelper.Runtime.clearTimeout(this.delayedClickTimerId);
this.countdownTimerId = null;
this.delayedClickTimerId = null;
},
scanAndClick: function() {
EducationHelper.Logger.action("自动扫描评估按钮开始...");
this.stopPendingClick();
EducationHelper.Preview.clear();
EducationHelper.EvaluationList.progress.updateProgress();
const evaluationButtons = EducationHelper.EvaluationList.itemHelpers.getPendingActions();
EducationHelper.Status.setRecognized(`待评入口 ${evaluationButtons.length} 个`);
EducationHelper.Logger.action(`找到 ${evaluationButtons.length} 个评估按钮`);
if (evaluationButtons.length > 0) {
if (EducationHelper.Config.state.dryRunMode) {
EducationHelper.Status.setNextAction("演练模式下仅预览待评入口");
EducationHelper.Utils.handleDryRun(
evaluationButtons,
`已识别 ${evaluationButtons.length} 个待评入口,演练模式下不会点击`
);
EducationHelper.UI.showMessage(
`演练模式
已识别 ${evaluationButtons.length} 个待评入口,仅预览不点击
`,
"info",
2500
);
return;
}
const buttonToClick = evaluationButtons[0];
let countdownSeconds = 3;
const statusMsg = EducationHelper.UI.showMessage(`
已找到待评入口
将在 ${countdownSeconds} 秒后点击
`, "info", 0);
this.countdownTimerId = EducationHelper.Runtime.setInterval(() => {
countdownSeconds--;
if (EducationHelper.Config.state.autoClickStopped) {
this.stopPendingClick();
if (statusMsg && document.contains(statusMsg)) {
statusMsg.remove();
}
return;
}
const countdownElement = document.getElementById("clickCountdown");
if (countdownElement) {
countdownElement.textContent = countdownSeconds;
}
if (countdownSeconds <= 0) {
EducationHelper.Runtime.clearInterval(this.countdownTimerId);
this.countdownTimerId = null;
if (statusMsg && document.contains(statusMsg)) {
statusMsg.innerHTML = '正在点击评估按钮...
';
}
this.delayedClickTimerId = EducationHelper.Runtime.setTimeout(() => {
if (!EducationHelper.Config.state.autoClickStopped) {
this.clickButton(buttonToClick);
if (statusMsg && document.contains(statusMsg)) {
statusMsg.remove();
}
}
}, 500);
}
}, 1e3);
} else {
EducationHelper.UI.showMessage("未找到评估按钮", "warning", 3e3);
}
},
clickButton: function(button) {
EducationHelper.Logger.action(`点击按钮: ${button.outerHTML}`);
if (EducationHelper.Utils.clickElement(button)) {
EducationHelper.Status.setNextAction("已点击待评入口,等待进入评估页面");
EducationHelper.Logger.success("已通过事件触发点击按钮");
return true;
}
EducationHelper.Logger.error("无法点击按钮");
return false;
}
},
// 自动模式控制
autoMode: {
enable: function() {
EducationHelper.Config.state.autoMode = true;
EducationHelper.Config.state.autoClickEvaluationEnabled = true;
EducationHelper.Config.state.autoClickStopped = false;
EducationHelper.Logger.action("全自动模式已启用");
const checkbox = document.getElementById("autoClickEvaluation");
if (checkbox) checkbox.checked = true;
EducationHelper.Config.saveSettings();
const countdownDiv = document.getElementById("countdownDiv");
if (countdownDiv) {
countdownDiv.style.display = "block";
countdownDiv.innerHTML = `
自动流程已启动
将在 ${EducationHelper.Config.timers.autoClickEvaluationDelay / 1e3} 秒后开始执行
`;
}
EducationHelper.EvaluationList.autoClicker.startCountdown();
EducationHelper.UI.showMessage(`
自动流程已启动
脚本会自动进入待评项、填写内容并提交。
需要时可随时停止。
`, "success", 3500);
return true;
},
disable: function() {
EducationHelper.Config.state.autoMode = false;
EducationHelper.Status.setNextAction("等待手动开始");
EducationHelper.Logger.action("全自动模式已禁用");
EducationHelper.shutdown("停止全自动模式", { keepProgressTracking: true });
EducationHelper.UI.showMessage(`
自动流程已停止
后续操作将保持手动模式。
`, "error", 2600);
return true;
}
},
generateUI: function() {
return `
${EducationHelper.Status.generatePanel()}
演练模式(只识别,不点击)
自动点击评估按钮
${EducationHelper.Config.state.autoMode ? "停止自动流程" : "启动自动流程"}
自动操作倒计时
${EducationHelper.Config.timers.autoClickEvaluationDelay / 1e3} 秒
操作提示
自动点击默认关闭。全自动模式会依次进入待评项、填写内容并提交,操作过程中可随时取消。
`;
},
bindEvents: function() {
document.getElementById("evaluationListDryRunMode").addEventListener("change", function() {
EducationHelper.Config.state.dryRunMode = this.checked;
EducationHelper.Config.saveSettings();
EducationHelper.Status.syncFromState();
EducationHelper.Logger.action(`评估列表演练模式: ${this.checked ? "已启用" : "已关闭"}`);
});
document.getElementById("autoClickEvaluation").addEventListener("change", function() {
EducationHelper.Config.state.autoClickEvaluationEnabled = this.checked;
EducationHelper.Config.state.autoClickStopped = !this.checked;
EducationHelper.Status.setNextAction(
this.checked ? EducationHelper.Config.state.dryRunMode ? "等待倒计时后预览评估入口" : "等待倒计时后点击评估入口" : "等待手动开始"
);
EducationHelper.Logger.action(`自动点击评估按钮: ${this.checked ? "已启用" : "已禁用"}`);
const countdownDiv = document.getElementById("countdownDiv");
if (this.checked) {
countdownDiv.style.display = "block";
EducationHelper.EvaluationList.autoClicker.startCountdown();
} else {
EducationHelper.Status.setNextAction("暂未识别到待评项目");
countdownDiv.style.display = "none";
EducationHelper.EvaluationList.autoClicker.stopCountdown();
EducationHelper.EvaluationList.scanner.stopPendingClick();
EducationHelper.Status.setNextAction("等待手动开始");
}
EducationHelper.Config.saveSettings();
EducationHelper.Status.syncFromState();
});
document.getElementById("autoModeButton").addEventListener("click", function() {
if (EducationHelper.Config.state.autoMode) {
EducationHelper.EvaluationList.autoMode.disable();
} else {
EducationHelper.EvaluationList.autoMode.enable();
}
this.style.background = EducationHelper.Config.state.autoMode ? "#DC2626" : "#2563EB";
this.textContent = EducationHelper.Config.state.autoMode ? "停止自动流程" : "启动自动流程";
EducationHelper.Status.syncFromState();
});
},
init: function() {
if (!EducationHelper.Config.pageType.isEvaluationListPage) return;
EducationHelper.Status.setNextAction("等待扫描评估条目");
EducationHelper.Status.syncFromState();
EducationHelper.Logger.action("初始化评估列表模块...");
this.progress.startTracking();
EducationHelper.Runtime.setTimeout(() => {
this.progress.updateProgress();
}, 200);
if (EducationHelper.Config.state.autoClickEvaluationEnabled || EducationHelper.Config.state.autoMode) {
EducationHelper.Runtime.setTimeout(() => {
const countdownDiv = document.getElementById("countdownDiv");
if (countdownDiv) {
countdownDiv.style.display = "block";
this.autoClicker.startCountdown();
}
}, 500);
}
return this;
}
};
EducationHelper.GpaCalculator = {
// 五级制文字成绩 → 数值映射
textGradeMap: {
"优秀": 95,
"优": 95,
"良好": 85,
"良": 85,
"中等": 75,
"中": 75,
"及格": 65,
"不及格": 0,
"不通过": 0
},
// 排除项:通过/免修等不计入 GPA
excludedGrades: ["通过", "免修", "缓考", "缺考", "取消", ""],
// GPA 算法:标准 4.0 制 (score-50)/10,上限 4.0
scoreToGpa: function(score) {
if (score < 60) return 0;
return Math.min(4, (score - 50) / 10);
},
// 解析成绩文本为数值
parseGrade: function(gradeText) {
const trimmed = gradeText.trim();
const num = parseFloat(trimmed);
if (!isNaN(num)) return { score: num, excluded: false };
if (this.textGradeMap.hasOwnProperty(trimmed)) {
return { score: this.textGradeMap[trimmed], excluded: false };
}
if (this.excludedGrades.includes(trimmed)) {
return { score: 0, excluded: true };
}
return { score: 0, excluded: true };
},
_degradeWarning: "",
// 表头关键词 → 字段名映射
headerMapping: [
{ field: "courseId", keywords: ["课程号", "课程代码", "课程编号", "课号"] },
{ field: "courseName", keywords: ["课程名", "课程名称"] },
{ field: "courseAttr", keywords: ["课程属性", "属性", "课程性质"] },
{ field: "credit", keywords: ["学分"] },
{ field: "gradeText", keywords: ["成绩", "总成绩", "总评成绩"] },
{ field: "gpaValue", keywords: ["绩点"] }
],
// 从表头自动检测列索引
detectColumns: function(table) {
const ths = table.querySelectorAll("thead th, thead td");
if (ths.length === 0) return null;
const colMap = {};
const headers = Array.from(ths).map((th) => th.textContent.trim());
this.headerMapping.forEach(({ field, keywords }) => {
for (let i = 0; i < headers.length; i++) {
if (keywords.some((kw) => headers[i].includes(kw))) {
colMap[field] = i;
break;
}
}
});
const required = ["courseName", "credit", "gradeText"];
const missing = required.filter((f) => colMap[f] === void 0);
if (missing.length > 0) {
EducationHelper.Logger.debug("表头缺少必要列: " + missing.join(", ") + ",表头为: " + headers.join(" | "));
return null;
}
EducationHelper.Logger.debug("表头自适应检测结果: " + JSON.stringify(colMap) + ",表头: " + headers.join(" | "));
return colMap;
},
// 从页面表格抓取成绩数据
scrapeScores: function() {
this._degradeWarning = "";
var cfgSelectors = EducationHelper.Config.selectors.scoreTable || [];
const selectors = cfgSelectors.concat([
"table.table-striped"
]);
let tables = null;
for (const sel of selectors) {
const found = document.querySelectorAll(sel);
if (found.length > 0) {
tables = found;
EducationHelper.Logger.debug("成绩表格匹配选择器: " + sel + ",共 " + found.length + " 张表");
break;
}
}
const courses = [];
if (!tables) {
this._degradeWarning = "⚠️ 未找到成绩表格。页面结构可能已更新,请联系脚本开发者。";
EducationHelper.Logger.error("成绩表格选择器全部失败");
return courses;
}
tables.forEach((table) => {
const colMap = this.detectColumns(table);
if (!colMap) {
this._degradeWarning = "⚠️ 表格列结构无法识别(表头变化)。请联系脚本开发者适配。";
return;
}
const rows = table.querySelectorAll("tbody tr");
const hasOfficialGpa = colMap.gpaValue !== void 0;
rows.forEach((row) => {
const cells = row.querySelectorAll("td");
if (cells.length <= Math.max(...Object.values(colMap))) return;
const courseId = colMap.courseId !== void 0 ? cells[colMap.courseId].textContent.trim() : "";
const courseName = cells[colMap.courseName].textContent.trim();
const courseAttr = colMap.courseAttr !== void 0 ? cells[colMap.courseAttr].textContent.trim() : "";
const credit = parseFloat(cells[colMap.credit].textContent.trim()) || 0;
const gradeText = cells[colMap.gradeText].textContent.trim();
const parsed = this.parseGrade(gradeText);
let gpa = null;
let excluded = parsed.excluded;
if (hasOfficialGpa) {
const officialGpa = parseFloat(cells[colMap.gpaValue].textContent.trim());
if (!isNaN(officialGpa)) {
gpa = officialGpa;
excluded = false;
}
}
if (gpa === null && !excluded) {
gpa = this.scoreToGpa(parsed.score);
}
courses.push({
courseId,
courseName,
courseAttr,
credit,
gradeText,
score: parsed.score,
excluded,
gpa,
hasOfficialGpa: hasOfficialGpa && gpa !== null
});
});
});
return courses;
},
// 按课程属性分组计算
calculate: function(courses, filter) {
let filtered = courses.filter((c) => !c.excluded && c.credit > 0);
if (filter && filter !== "全部") {
filtered = filtered.filter((c) => c.courseAttr === filter);
}
if (filtered.length === 0) return { gpa: 0, totalCredit: 0, count: 0 };
const totalWeighted = filtered.reduce((sum, c) => sum + c.gpa * c.credit, 0);
const totalCredit = filtered.reduce((sum, c) => sum + c.credit, 0);
return {
gpa: totalCredit > 0 ? totalWeighted / totalCredit : 0,
totalCredit,
count: filtered.length
};
},
getAttributes: function(courses) {
const attrs = /* @__PURE__ */ new Set();
courses.forEach((c) => {
if (c.courseAttr) attrs.add(c.courseAttr);
});
return ["全部", ...Array.from(attrs)];
},
generateUI: function() {
const courses = this.scrapeScores();
this._courses = courses;
const attrs = this.getAttributes(courses);
const allResult = this.calculate(courses, "全部");
const usingOfficial = courses.some((c) => c.hasOfficialGpa);
const degradeHtml = this._degradeWarning ? `
${EducationHelper.escapeHtml(this._degradeWarning)}
` : "";
let attrRows = "";
attrs.forEach((attr) => {
const r = this.calculate(courses, attr);
if (r.count === 0) return;
attrRows += `
${EducationHelper.escapeHtml(attr)}
${r.gpa.toFixed(3)}(${r.count}门 / ${r.totalCredit}学分)
`;
});
return `
${EducationHelper.Status.generatePanel()}
${degradeHtml}
绩点计算结果
${allResult.gpa.toFixed(3)}
加权平均绩点 · ${allResult.count}门 · ${allResult.totalCredit}学分
${usingOfficial ? "✅ 使用教务系统官方绩点" : "⚠️ 公式估算(表中未找到绩点列)"}
计算说明
${usingOfficial ? "数据来源 :直接读取成绩表中「绩点」列,与教务系统一致 " : "算法 :GPA = (百分制成绩 - 50) ÷ 10,上限 4.0(估算,可能与系统有差异) "}
五级制 :优秀→95, 良好→85, 中等→75, 及格→65
排除 :「通过」「免修」等无绩点课程不计入
加权 :按学分加权平均
全部课程明细(${courses.length}门)
课程
学分
成绩
绩点
${courses.map((c) => `
${EducationHelper.escapeHtml(c.courseName)}
${c.credit}
${EducationHelper.escapeHtml(c.gradeText)}
= 3 ? "#16A34A" : c.gpa >= 2 ? "#D97706" : "#DC2626"};">${c.excluded ? "-" : c.gpa.toFixed(1)}
`).join("")}
`;
},
bindEvents: function() {
},
refresh: function() {
const content = EducationHelper.UI.elements.content;
if (content) {
content.innerHTML = this.generateUI();
content.insertAdjacentHTML("beforeend", EducationHelper.UI.generateSettingsFooter());
EducationHelper.UI.bindSettingsFooter();
EducationHelper.UI.syncViewportConstraints();
}
},
init: function() {
if (!EducationHelper.Config.pageType.isScorePage) return;
EducationHelper.Logger.action("初始化绩点计算模块...");
const firstScrape = this.scrapeScores();
if (firstScrape.length === 0) {
EducationHelper.Logger.action("成绩表格未就绪,将延迟重试...");
EducationHelper.Status.setNextAction("等待页面渲染...");
EducationHelper.Status.syncFromState();
const self = this;
EducationHelper.Runtime.setTimeout(function() {
const retry1 = self.scrapeScores();
if (retry1.length > 0) {
EducationHelper.Logger.success("延迟重试成功,找到 " + retry1.length + " 门课程");
self.refresh();
return;
}
EducationHelper.Runtime.setTimeout(function() {
const retry2 = self.scrapeScores();
EducationHelper.Logger.action("第二次重试: " + retry2.length + " 门课程");
self.refresh();
}, 2e3);
}, 1e3);
} else {
EducationHelper.Status.setNextAction("成绩数据已解析(" + firstScrape.length + " 门)");
EducationHelper.Status.syncFromState();
}
}
};
EducationHelper.ScheduleEnhancer = {
_degradeWarning: "",
// 多选择器降级查找课程 div
_findCourseDivs: function(root) {
root = root || document;
const selectors = [
"#courseTableBody .class_div",
".table-course-detail .class_div",
"table .class_div",
".class_div"
];
for (const sel of selectors) {
const found = root.querySelectorAll(sel);
if (found.length > 0) {
EducationHelper.Logger.debug("课表课程块匹配选择器: " + sel + ",共 " + found.length + " 个");
return found;
}
}
return null;
},
// 从单个课程 div 中提取信息
_parseCourseDiv: function(div) {
let name = "";
const kcmEl = div.querySelector('[class*="kcm"]');
if (kcmEl) {
name = kcmEl.textContent.trim();
} else {
const allPs = div.querySelectorAll("p, div, span");
for (const p of allPs) {
const text = p.textContent.trim();
if (text && !p.className.includes("gray") && text.length > 1) {
name = text;
break;
}
}
}
const grayPs = div.querySelectorAll('[class*="gray"], [class*="kcb_p"]');
const grayTexts = Array.from(grayPs).map((p) => p.textContent.trim());
let location = "";
const jxlEl = div.querySelector('[class*="jxl"]');
if (jxlEl) {
location = jxlEl.textContent.trim();
} else {
location = grayTexts.find((t) => /楼|教室|实验|机房/.test(t)) || "";
}
let codeInfo = "", teacher = "", weeks = "", periods = "";
grayTexts.forEach((t) => {
if (/^\d{6,}/.test(t)) codeInfo = t;
else if (/周$|^\d+-\d+周/.test(t)) weeks = t;
else if (/节$|^\d+-\d+节/.test(t)) periods = t;
else if (!teacher && t !== location && t.length >= 2) teacher = t;
});
const td = div.closest("td");
const tdId = (td == null ? void 0 : td.id) || "";
const match = tdId.match(/(\d+)[_\-](\d+)/);
const weekday = match ? parseInt(match[1]) : 0;
const period = match ? parseInt(match[2]) : 0;
return { name, codeInfo, teacher, weeks, periods, location, weekday, period };
},
parseCourses: function(root) {
this._degradeWarning = "";
const divs = this._findCourseDivs(root);
if (!divs) {
this._degradeWarning = "⚠️ 未找到课表课程元素。页面结构可能已更新,请联系脚本开发者。";
EducationHelper.Logger.error("课表选择器全部失败");
return [];
}
const courses = [];
divs.forEach((div) => {
courses.push(this._parseCourseDiv(div));
});
return courses;
},
getTodayWeekday: function() {
const d = (/* @__PURE__ */ new Date()).getDay();
return d === 0 ? 7 : d;
},
highlightToday: function() {
const today = this.getTodayWeekday();
const headerSelectors = ["#courseTableHead th", ".table-course-detail thead th", "table thead th"];
let ths = null;
for (const sel of headerSelectors) {
const found = document.querySelectorAll(sel);
if (found.length >= 3) {
ths = found;
break;
}
}
if (ths && ths.length >= today + 3) {
ths[today + 2].style.backgroundColor = "#DBEAFE";
ths[today + 2].style.fontWeight = "700";
}
for (let period = 1; period <= 12; period++) {
const cell = document.getElementById(`${today}_${period}`);
if (cell && !cell.querySelector(".class_div") && !cell.querySelector('[class*="class"]')) {
cell.style.backgroundColor = "rgba(59, 130, 246, 0.06)";
}
}
},
getTodaySummary: function(courses) {
const today = this.getTodayWeekday();
const todayCourses = courses.filter((c) => c.weekday === today);
if (todayCourses.length === 0) return "今天没有课程 🎉";
todayCourses.sort((a, b) => a.period - b.period);
return todayCourses.map((c) => `${c.periods} ${c.name} · ${c.location}`).join(" ");
},
generateUI: function() {
const courses = this.parseCourses();
this._courses = courses;
const todaySummary = this.getTodaySummary(courses);
const weekdays = ["", "周一", "周二", "周三", "周四", "周五", "周六", "周日"];
const today = this.getTodayWeekday();
let allCoursesHtml = "";
const uniqueCourses = [];
const seen = /* @__PURE__ */ new Set();
courses.forEach((c) => {
if (!seen.has(c.codeInfo)) {
seen.add(c.codeInfo);
uniqueCourses.push(c);
}
});
uniqueCourses.forEach((c) => {
allCoursesHtml += `
${EducationHelper.escapeHtml(c.name)}
${EducationHelper.escapeHtml(c.teacher)} · ${EducationHelper.escapeHtml(c.weeks)} · ${EducationHelper.escapeHtml(c.periods)} · ${EducationHelper.escapeHtml(c.location)}
`;
});
const degradeHtml = this._degradeWarning ? `
${EducationHelper.escapeHtml(this._degradeWarning)}
` : "";
return `
${EducationHelper.Status.generatePanel()}
${degradeHtml}
今日课程(${EducationHelper.escapeHtml(weekdays[today])})
${todaySummary}
本学期课程(${uniqueCourses.length}门)
${allCoursesHtml || '
暂无课程数据
'}
增强说明
已自动高亮今天(${EducationHelper.escapeHtml(weekdays[today])})的列。
课程数据来自页面课表,无需额外操作。
`;
},
bindEvents: function() {
},
refresh: function() {
const content = EducationHelper.UI.elements.content;
if (content) {
content.innerHTML = this.generateUI();
content.insertAdjacentHTML("beforeend", EducationHelper.UI.generateSettingsFooter());
EducationHelper.UI.bindSettingsFooter();
EducationHelper.UI.syncViewportConstraints();
}
},
init: function() {
if (!EducationHelper.Config.pageType.isSchedulePage) return;
EducationHelper.Logger.action("初始化课表增强模块...");
this.highlightToday();
const firstParse = this.parseCourses();
if (firstParse.length === 0) {
EducationHelper.Logger.action("课表数据未就绪,将延迟重试...");
const self = this;
EducationHelper.Runtime.setTimeout(function() {
self.highlightToday();
const retry = self.parseCourses();
if (retry.length > 0) {
EducationHelper.Logger.success("课表延迟重试成功,找到 " + retry.length + " 门课程");
}
self.refresh();
}, 1500);
} else {
EducationHelper.Status.setNextAction("课表已增强(" + firstParse.length + " 门)");
EducationHelper.Status.syncFromState();
}
}
};
EducationHelper.Homepage = {
_navLinksCache: null,
getNavLinks: function() {
if (!this._navLinksCache) {
var u = EducationHelper.Config.urls;
this._navLinksCache = [
{ label: "📊 成绩查询", path: u.scoreQuery, desc: "查看方案成绩 & 绩点" },
{ label: "📅 本学期课表", path: u.schedule, desc: "查看课表 & 今日课程" },
{ label: "📝 教学评估", path: u.evaluationList, desc: "一键完成教学评估" },
{ label: "📚 选课入口", path: u.courseSelect, desc: "抢课 & 余量监控" },
{ label: "📋 培养方案", path: u.planCompletion, desc: "查看培养方案完成度" }
];
}
return this._navLinksCache;
},
_todayCourses: null,
_scheduleError: "",
_planData: null,
_planError: "",
_getTodayWeekday: function() {
const d = (/* @__PURE__ */ new Date()).getDay();
return d === 0 ? 7 : d;
},
_fetchPlanSummary: function(callback) {
var self = this;
var baseUrl = window.location.origin;
var url = baseUrl + EducationHelper.Config.urls.planCompletion;
EducationHelper.Logger.action("正在获取培养方案数据...");
fetch(url, { credentials: "same-origin" }).then(function(resp) {
if (!resp.ok) throw new Error("HTTP " + resp.status);
return resp.text();
}).then(function(html) {
var parser = new DOMParser();
var doc = parser.parseFromString(html, "text/html");
var sel = EducationHelper.Config.selectors;
var result = { summary: {}, groups: [] };
result.summary = EducationHelper.PlanCompletion._parseSummary(doc);
try {
var nodeSpans = doc.querySelectorAll(sel.planTreeNodes);
if (nodeSpans.length > 0) {
EducationHelper.Logger.debug("DOM 方式提取到 " + nodeSpans.length + " 个节点");
nodeSpans.forEach(function(span) {
var g = EducationHelper.PlanCompletion._parseGroupNode(span);
if (g) result.groups.push(g);
});
}
if (result.groups.length === 0) {
EducationHelper.Logger.debug("DOM 无节点,尝试正则提取...");
result.groups = EducationHelper.PlanCompletion._parseGroupsFromHtml(html);
EducationHelper.Logger.debug("正则提取到 " + result.groups.length + " 个课组节点");
}
EducationHelper.Logger.debug("最终解析出 " + result.groups.length + " 个课组");
} catch (e) {
EducationHelper.Logger.debug("解析课组失败: " + e.message);
}
EducationHelper.Logger.success("培养方案数据获取完成");
callback(result);
}).catch(function(err) {
EducationHelper.Logger.error("获取培养方案失败: " + err.message);
self._planError = "获取失败: " + err.message;
callback(null);
});
},
_renderPlanSummary: function() {
var esc = EducationHelper.escapeHtml;
if (this._planError) {
return '' + esc(this._planError) + "
";
}
if (!this._planData) {
return '⏳ 正在获取...
';
}
var data = this._planData;
var html = "";
var keys = Object.keys(data.summary);
if (keys.length > 0) {
html += '';
keys.forEach(function(key) {
html += '
';
html += '
' + esc(data.summary[key]) + "
";
html += '
' + esc(key) + "
";
html += "
";
});
html += "
";
}
if (data.groups.length > 0) {
var completed = data.groups.filter(function(g) {
return g.completed;
}).length;
var total = data.groups.length;
var incomplete = data.groups.filter(function(g) {
return !g.completed;
});
html += '课组 ' + completed + "/" + total + " 已完成
";
if (incomplete.length === 0) {
html += '🎉 所有课组已完成!
';
} else {
incomplete.forEach(function(g) {
var progress = g.minCredit > 0 ? Math.min(100, Math.round(g.passedCredit / g.minCredit * 100)) : 100;
var diff = Math.max(0, g.minCredit - g.passedCredit);
html += '';
html += '
';
html += '⬜ ' + esc(g.name) + " ";
html += '' + g.passedCredit + "/" + g.minCredit + " ";
html += "
";
html += '
";
var tips = [];
if (diff > 0) tips.push("差" + diff + "学分");
if (g.failedCourses > 0) tips.push(g.failedCourses + "门未及格");
if (g.missingCourses > 0) tips.push(g.missingCourses + "门缺修");
if (tips.length > 0) {
html += '
' + tips.join(" · ") + "
";
}
html += "
";
});
}
} else if (keys.length === 0) {
html += '未获取到培养方案数据
';
}
return html;
},
_fetchTodaySchedule: function(callback) {
var self = this;
var baseUrl = window.location.origin;
var url = baseUrl + EducationHelper.Config.urls.schedule;
var today = this._getTodayWeekday();
EducationHelper.Logger.action("正在获取今日课表...");
fetch(url, { credentials: "same-origin" }).then(function(resp) {
if (!resp.ok) throw new Error("HTTP " + resp.status);
return resp.text();
}).then(function(html) {
var parser = new DOMParser();
var doc = parser.parseFromString(html, "text/html");
var allCourses = EducationHelper.ScheduleEnhancer.parseCourses(doc);
var todayCourses = allCourses.filter(function(c) {
return c.weekday === today;
}).sort(function(a, b) {
return a.period - b.period;
});
if (allCourses.length === 0) {
self._scheduleError = "课表页未返回课程数据";
}
EducationHelper.Logger.success("今日课程获取完成,共 " + todayCourses.length + " 门");
callback(todayCourses);
}).catch(function(err) {
EducationHelper.Logger.error("获取课表失败: " + err.message);
self._scheduleError = "获取课表失败: " + err.message;
callback([]);
});
},
_renderSchedule: function(courses) {
const esc = EducationHelper.escapeHtml;
const weekDayNames = ["", "周一", "周二", "周三", "周四", "周五", "周六", "周日"];
const today = this._getTodayWeekday();
const dayLabel = weekDayNames[today] || "今天";
if (this._scheduleError) {
return '' + esc(this._scheduleError) + "
";
}
if (!courses || courses.length === 0) {
return '🎉 ' + dayLabel + "没有课,好好休息!
";
}
let html = "";
courses.forEach(function(c) {
html += '';
html += '
' + esc(c.periods || "第" + c.period + "节") + "
";
html += '
';
html += '
' + esc(c.name) + "
";
if (c.location) {
html += '
📍 ' + esc(c.location) + "
";
}
html += "
";
html += "
";
});
return html;
},
generateUI: function() {
const esc = EducationHelper.escapeHtml;
const weekDayNames = ["", "周一", "周二", "周三", "周四", "周五", "周六", "周日"];
const today = this._getTodayWeekday();
const dayLabel = weekDayNames[today] || "今天";
const scheduleContent = this._todayCourses !== null ? this._renderSchedule(this._todayCourses) : '⏳ 正在获取课表...
';
const baseUrl = window.location.origin;
const navButtons = this.getNavLinks().map((link) => `
${link.label}
`).join("");
return `
${EducationHelper.Status.generatePanel()}
📅 今日课程(${dayLabel})
${scheduleContent}
📋 培养方案
${this._renderPlanSummary()}
`;
},
bindEvents: function() {
},
refresh: function() {
const content = EducationHelper.UI.elements.content;
if (content) {
content.innerHTML = this.generateUI();
content.insertAdjacentHTML("beforeend", EducationHelper.UI.generateSettingsFooter());
EducationHelper.UI.bindSettingsFooter();
EducationHelper.UI.syncViewportConstraints();
}
},
init: function() {
if (!EducationHelper.Config.pageType.isHomePage) return;
EducationHelper.Logger.action("初始化首页仪表盘...");
const self = this;
this._fetchTodaySchedule(function(courses) {
self._todayCourses = courses;
self.refresh();
});
this._fetchPlanSummary(function(data) {
self._planData = data;
self.refresh();
});
EducationHelper.Status.setNextAction("首页就绪");
EducationHelper.Status.syncFromState();
}
};
EducationHelper.PlanCompletion = {
_degradeWarning: "",
_data: null,
// 从文档中解析培养方案摘要信息(Homepage 也会复用)
_parseSummary: function(doc) {
var summary = {};
var sel = EducationHelper.Config.selectors;
var tabOne = doc.querySelector(sel.planTabOne);
if (!tabOne) return summary;
tabOne.querySelectorAll(sel.planInfobox).forEach(function(box) {
var numEl = box.querySelector(sel.planInfoboxNum);
var labelEl = box.querySelector(sel.planInfoboxLabel);
if (numEl && labelEl) {
summary[labelEl.textContent.trim()] = numEl.textContent.trim();
}
var percentEl = box.querySelector(sel.planInfoboxPercent);
var pctLabel = box.querySelector(sel.planInfoboxPercentLabel);
if (percentEl && pctLabel) {
summary[pctLabel.textContent.trim()] = percentEl.textContent.trim();
}
});
tabOne.querySelectorAll(sel.planInfoboxSmall).forEach(function(box) {
var numEl = box.querySelector(sel.planInfoboxNum);
var labelEls = box.querySelectorAll(sel.planInfoboxLabel);
var percentEl = box.querySelector(sel.planInfoboxPercent);
if (numEl && labelEls.length > 0) {
var label = Array.from(labelEls).map(function(el) {
return el.textContent.trim();
}).join("");
summary[label] = numEl.textContent.trim();
}
if (percentEl && labelEls.length > 0) {
var pLabel = Array.from(labelEls).map(function(el) {
return el.textContent.trim();
}).join("");
summary[pLabel] = percentEl.textContent.trim();
}
});
return summary;
},
// 从单个课组节点解析基础信息(Homepage 也会复用)
_parseGroupNode: function(span) {
var pat = EducationHelper.Config.patterns;
var html = span.innerHTML;
var text = span.textContent.trim();
if (text.indexOf(pat.groupKeyword) === -1) return null;
var isCompleted = html.indexOf(pat.completedIcon) !== -1;
var nameM = text.match(pat.groupName);
var minM = text.match(pat.minCredit);
var passM = text.match(pat.passedCredit);
var failM = text.match(pat.failedCourses);
var missM = text.match(pat.missingCourses);
return {
name: nameM ? nameM[1].trim() : text,
completed: isCompleted,
minCredit: minM ? parseFloat(minM[1]) : 0,
passedCredit: passM ? parseFloat(passM[1]) : 0,
failedCourses: failM ? parseInt(failM[1]) : 0,
missingCourses: missM ? parseInt(missM[1]) : 0
};
},
// 从 HTML 原文中正则提取课组(DOM 无节点时的兜底,Homepage 也会复用)
_parseGroupsFromHtml: function(html) {
var self = this;
var pat = EducationHelper.Config.patterns;
var groups = [];
var nameBlockRegex = /"name"\s*:\s*"((?:[^"\\]|\\[\s\S])*)"/g;
var nameMatch;
while ((nameMatch = nameBlockRegex.exec(html)) !== null) {
if (nameMatch[1].indexOf(pat.groupKeyword) !== -1) {
var raw = nameMatch[1];
var decoded = raw.replace(/\\(.)/g, "$1");
var text = decoded.replace(/<[^>]+>/g, "").replace(/ /g, " ").trim();
var g = self._parseGroupNode({ innerHTML: decoded, textContent: text });
if (g) groups.push(g);
}
}
return groups;
},
// 从页面 DOM 抓取培养方案数据
scrapeData: function() {
this._degradeWarning = "";
var data = { summary: {}, groups: [] };
data.summary = this._parseSummary(document);
var sel = EducationHelper.Config.selectors;
var self = this;
var treeNodes = document.querySelectorAll(sel.planTreeRootNodes);
treeNodes.forEach(function(span) {
var g = self._parseGroupNode(span);
if (g) {
var text = span.textContent.trim();
var totalCourseMatch = text.match(/已修课程门数[::]?(\d+)/);
var passedCourseMatch = text.match(/已及格课程门数[::]?(\d+)/);
g.totalCourses = totalCourseMatch ? parseInt(totalCourseMatch[1]) : 0;
g.passedCourses = passedCourseMatch ? parseInt(passedCourseMatch[1]) : 0;
data.groups.push(g);
}
});
if (data.groups.length === 0) {
this._degradeWarning = "⚠️ 未找到培养方案课组数据。页面可能尚未加载完成或结构已变更。";
}
try {
var treeId = sel.planTreeContainer.replace(/^#/, "");
var treeObj = typeof $ !== "undefined" && $.fn && $.fn.zTree ? $.fn.zTree.getZTreeObj(treeId) : null;
if (treeObj) {
var rootNodes = treeObj.getNodes();
data.groups.forEach(function(group) {
group.missingCourseDetails = [];
if (group.completed) return;
var matchNode = null;
for (var i = 0; i < rootNodes.length; i++) {
var nodeText = rootNodes[i].name.replace(/<[^>]+>/g, "").replace(/ /g, " ").trim();
if (nodeText.indexOf(group.name) !== -1) {
matchNode = rootNodes[i];
break;
}
}
if (!matchNode) return;
var allCourses = [];
(function collect(nodes) {
if (!nodes) return;
nodes.forEach(function(n) {
if (n.flagType === "kch") {
allCourses.push(n);
}
if (n.children) collect(n.children);
});
})(matchNode.children);
allCourses.forEach(function(course) {
var nm = course.name;
var isMissing = nm.indexOf("fa-meh-o") !== -1;
var isFailed = nm.indexOf("fa-frown-o") !== -1;
if (!isMissing && !isFailed) return;
var plain = nm.replace(/<[^>]+>/g, "").replace(/ /g, " ").trim();
group.missingCourseDetails.push({
text: plain,
status: isFailed ? "failed" : "missing"
});
});
group.missingCourseDetails.sort(function(a, b) {
return a.status === "failed" ? -1 : b.status === "failed" ? 1 : 0;
});
});
}
} catch (e) {
EducationHelper.Logger.debug("zTree API 不可用: " + e.message);
}
return data;
},
generateUI: function() {
var esc = EducationHelper.escapeHtml;
var data = this.scrapeData();
this._data = data;
var degradeHtml = this._degradeWarning ? '' + esc(this._degradeWarning) + "
" : "";
var groups = data.groups;
var completedGroups = groups.filter(function(g) {
return g.completed;
});
var incompleteGroups = groups.filter(function(g) {
return !g.completed;
});
var totalMinCredit = groups.reduce(function(s, g) {
return s + g.minCredit;
}, 0);
var totalPassedCredit = groups.reduce(function(s, g) {
return s + g.passedCredit;
}, 0);
var overallPercent = totalMinCredit > 0 ? Math.min(100, totalPassedCredit / totalMinCredit * 100).toFixed(1) : "0.0";
var inPlan = data.summary["方案内修读课程门数"] || "-";
var outPlan = data.summary["方案外修读课程门数"] || "-";
var summaryHtml = '' + overallPercent + '%
学分完成度 · 已修 ' + totalPassedCredit + " / 要求 " + totalMinCredit + '
方案内' + inPlan + "门 · 方案外" + outPlan + "门 · 课组 " + completedGroups.length + "✅ " + incompleteGroups.length + "⏳
";
summaryHtml += '';
var incompleteHtml = "";
if (incompleteGroups.length > 0) {
incompleteGroups.forEach(function(g) {
var creditPercent = g.minCredit > 0 ? Math.min(100, g.passedCredit / g.minCredit * 100).toFixed(0) : "100";
var remaining = Math.max(0, g.minCredit - g.passedCredit);
incompleteHtml += '' + esc(g.name) + '
' + creditPercent + '%
学分 ' + g.passedCredit + "/" + g.minCredit + "(差" + remaining + ") · 缺修" + g.missingCourses + '门
';
var details = g.missingCourseDetails || [];
if (details.length > 0) {
var showLimit = 15;
var visibleItems = details.slice(0, showLimit);
var hiddenCount = details.length - showLimit;
incompleteHtml += '
';
visibleItems.forEach(function(d) {
var color = d.status === "failed" ? "#DC2626" : "var(--helper-muted)";
var icon = d.status === "failed" ? "❌" : "○";
incompleteHtml += '
' + icon + " " + esc(d.text) + "
";
});
if (hiddenCount > 0) {
incompleteHtml += '
…还有 ' + hiddenCount + " 门未修读
";
}
incompleteHtml += "
";
}
incompleteHtml += "
";
});
} else {
incompleteHtml = '🎉 所有课组已完成!
';
}
var completedHtml = "";
completedGroups.forEach(function(g) {
completedHtml += '✅ ' + esc(g.name) + ' ' + g.passedCredit + "/" + g.minCredit + "
";
});
return EducationHelper.Status.generatePanel() + degradeHtml + '📋 培养方案完成度
' + summaryHtml + "
" + (incompleteGroups.length > 0 ? '⏳ 未完成课组(' + incompleteGroups.length + ")
" + incompleteHtml + "
" : '' + incompleteHtml + "
") + '已完成课组(' + completedGroups.length + ') ' + completedHtml + "
";
},
bindEvents: function() {
},
refresh: function() {
var content = EducationHelper.UI.elements.content;
if (content) {
content.innerHTML = this.generateUI();
content.insertAdjacentHTML("beforeend", EducationHelper.UI.generateSettingsFooter());
EducationHelper.UI.bindSettingsFooter();
EducationHelper.UI.syncViewportConstraints();
}
},
init: function() {
if (!EducationHelper.Config.pageType.isPlanPage) return;
EducationHelper.Logger.action("初始化培养方案分析模块...");
var self = this;
var firstData = this.scrapeData();
if (firstData.groups.length === 0) {
EducationHelper.Logger.action("培养方案数据未就绪,延迟重试...");
EducationHelper.Runtime.setTimeout(function() {
self.refresh();
var retryData = self.scrapeData();
if (retryData.groups.length > 0) {
EducationHelper.Logger.success("培养方案数据加载成功,共 " + retryData.groups.length + " 个课组");
}
}, 1500);
}
EducationHelper.Status.setNextAction("培养方案分析就绪");
EducationHelper.Status.syncFromState();
}
};
EducationHelper.shutdown = function(reason = "manual-stop", options = {}) {
EducationHelper.Logger.action(`停止自动流程: ${reason}`);
const keepProgressTracking = options.keepProgressTracking === true;
EducationHelper.Config.state.autoMode = false;
EducationHelper.Config.state.autoClickEvaluationEnabled = false;
EducationHelper.Config.state.autoClickStopped = true;
if (EducationHelper.Config.state.timer) {
EducationHelper.Runtime.clearInterval(EducationHelper.Config.state.timer);
EducationHelper.Config.state.timer = null;
}
if (EducationHelper.Config.state.courseMonitorTimer) {
try {
EducationHelper.CourseGrabber.monitor.stop();
} catch (e) {
}
}
try {
EducationHelper.Evaluator.submitter.countdown.stop();
} catch (error) {
EducationHelper.Logger.debug("停止评价倒计时时忽略异常", error);
}
try {
EducationHelper.EvaluationList.autoClicker.stopCountdown();
EducationHelper.EvaluationList.scanner.stopPendingClick();
if (!keepProgressTracking) {
EducationHelper.EvaluationList.progress.stopTracking();
}
} catch (error) {
EducationHelper.Logger.debug("停止评估列表自动化时忽略异常", error);
}
const startButton = document.getElementById("startScript");
const stopButton = document.getElementById("stopScript");
if (startButton) startButton.disabled = false;
if (stopButton) stopButton.disabled = true;
const autoClickCheckbox = document.getElementById("autoClickEvaluation");
if (autoClickCheckbox) autoClickCheckbox.checked = false;
EducationHelper.Preview.clear();
EducationHelper.Status.setNextAction("等待用户重新开始");
EducationHelper.Config.saveSettings();
};
function registerKeyboardShortcuts() {
document.addEventListener("keydown", function(e) {
if (e.ctrlKey && e.shiftKey && e.key === "H") {
e.preventDefault();
const container = EducationHelper.UI.elements.container;
if (container) {
if (container.style.display === "none") {
container.style.display = "";
container.style.opacity = "1";
container.style.transform = "scale(1)";
} else {
EducationHelper.UI.toggleMinimize();
}
}
}
if (e.ctrlKey && e.shiftKey && e.key === "S") {
e.preventDefault();
if (EducationHelper.Config.state.autoMode) {
EducationHelper.shutdown("keyboard-shortcut");
EducationHelper.UI.showMessage("已通过快捷键停止自动流程", "warning", 2e3);
} else {
const startBtn = document.getElementById("startAutoMode");
if (startBtn) startBtn.click();
}
}
if (e.key === "Escape") {
if (EducationHelper.Config.state.autoMode || EducationHelper.Config.state.autoClickEvaluationEnabled) {
e.preventDefault();
EducationHelper.shutdown("escape-key");
EducationHelper.UI.showMessage("已通过 Esc 取消操作", "warning", 2e3);
}
}
});
}
function compareVersions(a, b) {
const pa = a.split(".").map(Number);
const pb = b.split(".").map(Number);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const na = pa[i] || 0;
const nb = pb[i] || 0;
if (na > nb) return 1;
if (na < nb) return -1;
}
return 0;
}
function checkForUpdate() {
try {
const currentVersion = "2.7.1";
const lastCheckKey = "lastUpdateCheck";
const lastCheck = EducationHelper.Storage.get(lastCheckKey, 0);
const now = Date.now();
if (now - lastCheck < 864e5) return;
EducationHelper.Storage.set(lastCheckKey, now);
EducationHelper.Logger.debug("检查更新中...");
if (typeof GM_xmlhttpRequest !== "function") {
EducationHelper.Logger.debug("GM_xmlhttpRequest 不可用,跳过更新检查");
return;
}
GM_xmlhttpRequest({
method: "GET",
url: "https://scriptcat.org/scripts/code/3312/%E9%BD%90%E5%A4%A7%E6%95%99%E5%8A%A1%E5%8A%A9%E6%89%8B.user.js",
headers: { "Cache-Control": "no-cache" },
onload: function(response) {
try {
const match = response.responseText.match(/@version\s+(\S+)/);
if (!match) return;
const remoteVersion = match[1];
if (compareVersions(remoteVersion, currentVersion) > 0) {
EducationHelper.Logger.info("发现新版本: v" + remoteVersion);
EducationHelper.Notification.send(
"发现新版本",
"当前 v" + currentVersion + " → 最新 v" + remoteVersion,
{ timeout: 1e4 }
);
} else {
EducationHelper.Logger.debug("当前已是最新版本: v" + currentVersion);
}
} catch (e) {
EducationHelper.Logger.debug("解析远程版本失败", e);
}
},
onerror: function() {
EducationHelper.Logger.debug("更新检查请求失败");
}
});
} catch (error) {
EducationHelper.Logger.debug("更新检查跳过", error);
}
}
const PAGE_MODULE_MAP = [
{ flag: "isCoursePage", module: "CourseGrabber" },
{ flag: "isEvaluationPage", module: "Evaluator" },
{ flag: "isEvaluationListPage", module: "EvaluationList" },
{ flag: "isScorePage", module: "GpaCalculator" },
{ flag: "isSchedulePage", module: "ScheduleEnhancer" },
{ flag: "isPlanPage", module: "PlanCompletion" },
{ flag: "isHomePage", module: "Homepage" }
];
function initModulesByPageType() {
const config = EducationHelper.Config;
const matched = PAGE_MODULE_MAP.find(({ flag }) => config.pageType[flag]);
if (matched) {
const mod = EducationHelper[matched.module];
EducationHelper.UI.elements.content.innerHTML = mod.generateUI();
EducationHelper.UI.syncViewportConstraints();
mod.bindEvents();
mod.init();
} else {
EducationHelper.UI.elements.content.innerHTML = `
`;
EducationHelper.UI.syncViewportConstraints();
}
EducationHelper.UI.elements.content.insertAdjacentHTML("beforeend", EducationHelper.UI.generateSettingsFooter());
EducationHelper.UI.bindSettingsFooter();
EducationHelper.UI.syncViewportConstraints();
}
function init() {
console.log("齐大教务助手启动中...");
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initAfterDOMLoaded);
} else {
initAfterDOMLoaded();
}
}
function initAfterDOMLoaded() {
try {
EducationHelper.Config.init();
EducationHelper.UI.create();
initModulesByPageType();
registerKeyboardShortcuts();
checkForUpdate();
} catch (error) {
console.error("[齐大教务助手] 初始化失败:", error);
EducationHelper.Notification.scriptError("脚本初始化失败: " + error.message);
}
}
init();
})();