// ==UserScript== // @name 齐大教务助手 // @namespace https://greasyfork.org/users/737539 // @version 2.7.1 // @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/* // @grant GM_deleteValue // @grant GM_getValue // @grant GM_notification // @grant GM_setValue // ==/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 storedSettings = EducationHelper.Storage.get("settings", {}); if (storedSettings.autoMode === true) { this.state.autoMode = true; console.log("[检测] 从localStorage检测到全自动模式已启用"); } if (typeof storedSettings.autoClickEvaluationEnabled === "boolean") { this.state.autoClickEvaluationEnabled = storedSettings.autoClickEvaluationEnabled; } if (typeof storedSettings.autoSubmitEnabled === "boolean") { this.state.autoSubmitEnabled = storedSettings.autoSubmitEnabled; } if (typeof storedSettings.dryRunMode === "boolean") { this.state.dryRunMode = storedSettings.dryRunMode; } if (typeof storedSettings.debugMode === "boolean") { this.state.debugMode = storedSettings.debugMode; } if (typeof storedSettings.uiFollowPage === "boolean") { this.state.uiFollowPage = storedSettings.uiFollowPage; } if (storedSettings.panelPosition) { this.state.panelPosition = storedSettings.panelPosition; } if (storedSettings.evaluationComment) { this.content.evaluationComment = storedSettings.evaluationComment; } if (typeof storedSettings.useRandomTemplate === "boolean") { this.content.useRandomTemplate = storedSettings.useRandomTemplate; } if (storedSettings.selectedOption) { this.state.selectedOption = storedSettings.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 '
更多设置
评估默认选项
' + ["A", "B", "C"].map(function(v) { var active = cfg.state.selectedOption === v; return '"; }).join("") + '
评语内容
' + 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 '
"; }, 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 = ""; }); } }, // 创建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.width = "300px"; this.elements.container.style.backgroundColor = "#FFFFFF"; this.elements.container.style.border = "1px solid #DBE3EC"; this.elements.container.style.padding = "0"; this.elements.container.style.zIndex = "9999"; this.elements.container.style.boxShadow = "0 10px 24px rgba(15, 23, 42, 0.08)"; this.elements.container.style.borderRadius = "14px"; this.elements.container.style.fontFamily = '"Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif'; this.elements.container.style.backdropFilter = "none"; this.elements.container.style.webkitBackdropFilter = "none"; 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()}
评估进度
0/0
正在统计评价进度...
自动操作倒计时
${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 ? "✅ 使用教务系统官方绩点" : "⚠️ 公式估算(表中未找到绩点列)"}
分类绩点
${attrRows}
计算说明
${usingOfficial ? "数据来源:直接读取成绩表中「绩点」列,与教务系统一致
" : "算法:GPA = (百分制成绩 - 50) ÷ 10,上限 4.0(估算,可能与系统有差异)
"} 五级制:优秀→95, 良好→85, 中等→75, 及格→65
排除:「通过」「免修」等无绩点课程不计入
加权:按学分加权平均
全部课程明细(${courses.length}门)
${courses.map((c) => ` `).join("")}
课程 学分 成绩 绩点
${EducationHelper.escapeHtml(c.courseName)} ${c.credit} ${EducationHelper.escapeHtml(c.gradeText)} = 3 ? "#16A34A" : c.gpa >= 2 ? "#D97706" : "#DC2626"};">${c.excluded ? "-" : c.gpa.toFixed(1)}
`; }, 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() { const selectors = [ "#courseTableBody .class_div", ".table-course-detail .class_div", "table .class_div", ".class_div" ]; for (const sel of selectors) { const found = document.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() { this._degradeWarning = ""; const divs = this._findCourseDivs(); 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 result = { summary: {}, groups: [] }; var sel = EducationHelper.Config.selectors; var pat = EducationHelper.Config.patterns; var tabOne = doc.querySelector(sel.planTabOne); if (tabOne) { tabOne.querySelectorAll(sel.planInfobox).forEach(function(box) { var numEl = box.querySelector(sel.planInfoboxNum); var labelEl = box.querySelector(sel.planInfoboxLabel); if (numEl && labelEl) { result.summary[labelEl.textContent.trim()] = numEl.textContent.trim(); } var percentEl = box.querySelector(sel.planInfoboxPercent); var pctLabel = box.querySelector(sel.planInfoboxPercentLabel); if (percentEl && pctLabel) { result.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(""); result.summary[label] = numEl.textContent.trim(); } if (percentEl && labelEls.length > 0) { var pLabel = Array.from(labelEls).map(function(el) { return el.textContent.trim(); }).join(""); result.summary[pLabel] = percentEl.textContent.trim(); } }); } try { var parseGroup = function(rawHtml, plainText) { if (plainText.indexOf(pat.groupKeyword) === -1) return null; var isCompleted = rawHtml.indexOf(pat.completedIcon) !== -1; var nameM = plainText.match(pat.groupName); var minM = plainText.match(pat.minCredit); var passM = plainText.match(pat.passedCredit); var failM = plainText.match(pat.failedCourses); var missM = plainText.match(pat.missingCourses); return { name: nameM ? nameM[1].trim() : plainText, 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 }; }; var nodeSpans = doc.querySelectorAll(sel.planTreeNodes); if (nodeSpans.length > 0) { EducationHelper.Logger.debug("DOM 方式提取到 " + nodeSpans.length + " 个节点"); nodeSpans.forEach(function(span) { var g = parseGroup(span.innerHTML, span.textContent.trim()); if (g) result.groups.push(g); }); } if (result.groups.length === 0) { EducationHelper.Logger.debug("DOM 无节点,尝试正则提取..."); var nameBlockRegex = /"name"\s*:\s*"((?:[^"\\]|\\[\s\S])*)"/g; var nameMatch; var rawNames = []; while ((nameMatch = nameBlockRegex.exec(html)) !== null) { if (nameMatch[1].indexOf(pat.groupKeyword) !== -1) { rawNames.push(nameMatch[1]); } } EducationHelper.Logger.debug("正则提取到 " + rawNames.length + " 个课组节点"); rawNames.forEach(function(raw) { var decoded = raw.replace(/\\(.)/g, "$1"); var text = decoded.replace(/<[^>]+>/g, "").replace(/ /g, " ").trim(); var g = parseGroup(decoded, text); if (g) result.groups.push(g); }); } 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 += '
'; 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) { const self = this; const baseUrl = window.location.origin; const url = baseUrl + EducationHelper.Config.urls.schedule; const 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) { const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); const selectors = ["#courseTableBody .class_div", ".table-course-detail .class_div", "table .class_div", ".class_div"]; let divs = null; for (const sel of selectors) { const found = doc.querySelectorAll(sel); if (found.length > 0) { divs = found; break; } } if (!divs || divs.length === 0) { self._scheduleError = "课表页未返回课程数据"; callback([]); return; } const todayCourses = []; divs.forEach(function(div) { const td = div.closest("td"); const tdId = td ? td.id : ""; const match = tdId.match(/(\d+)[_\-](\d+)/); const weekday = match ? parseInt(match[1]) : 0; const period = match ? parseInt(match[2]) : 0; if (weekday !== today) return; let name = ""; const kcmEl = div.querySelector('[class*="kcm"]'); if (kcmEl) { name = kcmEl.textContent.trim(); } else { const allEls = div.querySelectorAll("p, div, span"); for (const el of allEls) { const t = el.textContent.trim(); if (t && !el.className.includes("gray") && t.length > 1) { name = t; break; } } } let location = ""; const jxlEl = div.querySelector('[class*="jxl"]'); if (jxlEl) { location = jxlEl.textContent.trim(); } else { const grayPs = div.querySelectorAll('[class*="gray"], [class*="kcb_p"]'); for (const p of grayPs) { const t = p.textContent.trim(); if (/楼|教室|实验|机房/.test(t)) { location = t; break; } } } let periods = ""; const grayAll = div.querySelectorAll('[class*="gray"], [class*="kcb_p"]'); for (const p of grayAll) { const t = p.textContent.trim(); if (/节$|^\d+-\d+节/.test(t)) { periods = t; break; } } todayCourses.push({ name, location, periods, period }); }); todayCourses.sort(function(a, b) { return a.period - b.period; }); 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()}
快捷导航
${navButtons}
`; }, 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, // 从页面 DOM 抓取培养方案数据 scrapeData: function() { this._degradeWarning = ""; var data = { summary: {}, groups: [] }; var sel = EducationHelper.Config.selectors; var pat = EducationHelper.Config.patterns; var tabOne = document.querySelector(sel.planTabOne); if (tabOne) { tabOne.querySelectorAll(sel.planInfobox).forEach(function(box) { var numEl = box.querySelector(sel.planInfoboxNum); var labelEl = box.querySelector(sel.planInfoboxLabel); if (numEl && labelEl) { data.summary[labelEl.textContent.trim()] = numEl.textContent.trim(); } var percentEl = box.querySelector(sel.planInfoboxPercent); var pctLabel = box.querySelector(sel.planInfoboxPercentLabel); if (percentEl && pctLabel) { data.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(""); data.summary[label] = numEl.textContent.trim(); } if (percentEl && labelEls.length > 0) { var pLabel = Array.from(labelEls).map(function(el) { return el.textContent.trim(); }).join(""); data.summary[pLabel] = percentEl.textContent.trim(); } }); } var treeNodes = document.querySelectorAll(sel.planTreeRootNodes); treeNodes.forEach(function(span) { var html = span.innerHTML; var text = span.textContent.trim(); var isCompleted = html.indexOf(pat.completedIcon) !== -1; var nameMatch = text.match(pat.groupName); var name = nameMatch ? nameMatch[1].trim() : text; var minCreditMatch = text.match(pat.minCredit); var passedCreditMatch = text.match(pat.passedCredit); var totalCourseMatch = text.match(/已修课程门数[::]?(\d+)/); var passedCourseMatch = text.match(/已及格课程门数[::]?(\d+)/); var failedCourseMatch = text.match(pat.failedCourses); var missingMatch = text.match(pat.missingCourses); data.groups.push({ name, completed: isCompleted, minCredit: minCreditMatch ? parseFloat(minCreditMatch[1]) : 0, passedCredit: passedCreditMatch ? parseFloat(passedCreditMatch[1]) : 0, totalCourses: totalCourseMatch ? parseInt(totalCourseMatch[1]) : 0, passedCourses: passedCourseMatch ? parseInt(passedCourseMatch[1]) : 0, failedCourses: failedCourseMatch ? parseInt(failedCourseMatch[1]) : 0, missingCourses: missingMatch ? parseInt(missingMatch[1]) : 0 }); }); 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 checkForUpdate() { try { const currentVersion = "2.6.0"; 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("检查更新中..."); } catch (error) { EducationHelper.Logger.debug("更新检查跳过", error); } } function initModulesByPageType() { const config = EducationHelper.Config; if (config.pageType.isCoursePage) { EducationHelper.UI.elements.content.innerHTML = EducationHelper.CourseGrabber.generateUI(); EducationHelper.UI.syncViewportConstraints(); EducationHelper.CourseGrabber.bindEvents(); EducationHelper.CourseGrabber.init(); } else if (config.pageType.isEvaluationPage) { EducationHelper.UI.elements.content.innerHTML = EducationHelper.Evaluator.generateUI(); EducationHelper.UI.syncViewportConstraints(); EducationHelper.Evaluator.bindEvents(); EducationHelper.Evaluator.init(); } else if (config.pageType.isEvaluationListPage) { EducationHelper.UI.elements.content.innerHTML = EducationHelper.EvaluationList.generateUI(); EducationHelper.UI.syncViewportConstraints(); EducationHelper.EvaluationList.bindEvents(); EducationHelper.EvaluationList.init(); } else if (config.pageType.isScorePage) { EducationHelper.UI.elements.content.innerHTML = EducationHelper.GpaCalculator.generateUI(); EducationHelper.UI.syncViewportConstraints(); EducationHelper.GpaCalculator.bindEvents(); EducationHelper.GpaCalculator.init(); } else if (config.pageType.isSchedulePage) { EducationHelper.UI.elements.content.innerHTML = EducationHelper.ScheduleEnhancer.generateUI(); EducationHelper.UI.syncViewportConstraints(); EducationHelper.ScheduleEnhancer.bindEvents(); EducationHelper.ScheduleEnhancer.init(); } else if (config.pageType.isPlanPage) { EducationHelper.UI.elements.content.innerHTML = EducationHelper.PlanCompletion.generateUI(); EducationHelper.UI.syncViewportConstraints(); EducationHelper.PlanCompletion.bindEvents(); EducationHelper.PlanCompletion.init(); } else if (config.pageType.isHomePage) { EducationHelper.UI.elements.content.innerHTML = EducationHelper.Homepage.generateUI(); EducationHelper.UI.syncViewportConstraints(); EducationHelper.Homepage.bindEvents(); EducationHelper.Homepage.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(); })();