// ==UserScript== // @name 齐大教务助手 // @namespace https://greasyfork.org/users/737539 // @version 2.7.0 // @description 集成抢课功能与教学评估功能,一体化教务助手 // @author 忘忧 // @icon https://xyh.qqhru.edu.cn/favicon.ico // @license MIT // @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_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_notification // ==/UserScript== (function () { 'use strict'; // ================================================================ // 🔧 集中配置区 —— 学校 URL 或页面结构变更时只需修改此处 // ================================================================ // 页面路由关键词(用于 URL 匹配检测页面类型) const ROUTES = { evaluationPage: 'teachingEvaluation/evaluationPage', // 单个评估页 evaluationList: ['teachingEvaluation/teachingEvaluation/index', 'teachingEvaluation/evaluation/index'], // 评估列表页 scorePage: 'integratedQuery/scoreQuery', // 成绩查询页 schedulePage: ['thisSemesterCurriculum', 'calendarSemesterCurriculum'], // 课表页 planPage: 'integratedQuery/planCompletion', // 培养方案页 coursePage: 'courseSelect', // 选课页(排除课表) }; // 页面 URL 路径(用于 fetch 和导航链接) 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', }; // DOM 选择器(页面结构变更时修改) 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', }; // 课组数据提取模式(从 HTML 中提取 zTree 课组数据) const PATTERNS = { groupKeyword: '最低修读学分', // 课组节点关键词 completedIcon: 'fa-check-square-o', // 已完成课组图标 class minCredit: /最低修读学分[::]?([\d.]+)/, passedCredit: /通过学分[::]?([\d.]+)/, failedCourses: /未及格课程门数[::]?(\d+)/, missingCourses: /必修课缺修门数[::]?(\d+)/, groupName: /\s*(.+?)\(/, // 课组名提取(括号前的文本) }; // ================================================================ // 以下为脚本核心代码,一般不需要修改 // ================================================================ 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 || 1000; 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 = []) { return candidates .map(candidate => ({ candidate, score: this.scoreBackCandidate(candidate), })) .sort((a, b) => b.score - a.score)[0]?.candidate || null; } }; const isNodeLikeEnv = typeof window === 'undefined' || typeof document === 'undefined'; if (typeof module !== 'undefined' && module.exports) { module.exports = BrowserHelperCore; } if (isNodeLikeEnv) { return; } const HELPER_INSTANCE_KEY = '__qqhruEducationHelperLoaded__'; if (window[HELPER_INSTANCE_KEY] || document.getElementById('qqhruHelperUI')) { console.log('齐大教务助手已加载,跳过重复初始化。'); return; } window[HELPER_INSTANCE_KEY] = true; // 全局错误边界 - 捕获未处理的运行时异常 window.addEventListener('error', (event) => { console.error('[齐大教务助手] 运行时错误:', event.message, event.filename, event.lineno); }); window.addEventListener('unhandledrejection', (event) => { console.error('[齐大教务助手] 未处理的 Promise 异常:', event.reason); }); /** * 齐大教务助手主模块 * 模块化结构,将不同功能划分为独立模块 */ const EducationHelper = { // 系统配置 Config: { // 引用顶部集中配置区的常量(修改请到文件第 23 行) 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: 10000, // 延迟10秒执行自动点击 autoSubmitDelay: 120000, // 自动提交延迟,默认2分钟 }, // 内容设置 content: { evaluationComment: "上课有热情,积极解决学生问题,很好的老师!!", // 默认评价内容 // 评教模板库 - 随机模式下从中选取 evaluationTemplates: [ "老师上课讲解清晰,条理分明,课堂气氛活跃,能够很好地调动同学们的学习积极性。", "课程内容丰富充实,老师备课认真,教学态度端正,对学生耐心负责,值得称赞。", "老师教学经验丰富,善于用生动的例子帮助理解抽象概念,课堂内容深入浅出。", "教学方法灵活多样,注重理论联系实际,拓宽了我们的知识面和视野,受益匪浅。", "老师对待教学认真负责,课上互动积极,课下答疑耐心,是一位优秀的老师。", "上课有热情,积极解决学生问题,讲课逻辑清晰,重点突出,学到了很多知识。", "老师授课风格独特,课堂生动有趣,能够激发我们的学习兴趣和思考能力。", "课程安排合理,教学进度适当,老师注重培养我们的实践能力,教学效果很好。", "老师为人和蔼可亲,上课幽默风趣,与同学们关系融洽,教学水平高,值得尊敬。", "课堂氛围轻松活跃,老师鼓励学生表达观点,培养了我们的独立思考能力,感谢老师的付出。", ], useRandomTemplate: false, // 是否使用随机模板 }, // 初始化配置 init: function() { this.pageType.currentPageUrl = window.location.href; var url = this.pageType.currentPageUrl; var r = this.routes; // 辅助:URL 包含关键词(支持字符串或数组) var urlHas = function(keywords) { if (Array.isArray(keywords)) return keywords.some(function(k) { return url.includes(k); }); return url.includes(keywords); }; // 检测页面类型(使用顶部集中配置的 routes) 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); // 首页检测:根路径 / 或 /student/index 或 /student/ this.pageType.isHomePage = /\/(student(\/index)?)?\/?([?#].*)?$/.test(url) && !this.pageType.isEvaluationPage && !this.pageType.isEvaluationListPage && !this.pageType.isScorePage && !this.pageType.isSchedulePage; // 选课页检测放最后:排除课表页(URL 也含 courseSelect) this.pageType.isCoursePage = urlHas(r.coursePage) && !this.pageType.isSchedulePage; // 从localStorage读取持久化设置 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); } } }, // 存储模块 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 !== undefined) { 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); } }, // 日志模块 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) { console.log(`[信息] ${message}`); EducationHelper.Status?.setLastAction(message); }, // 操作日志 action: function(message) { console.log(`[操作] ${message}`); EducationHelper.Status?.setLastAction(message); }, // 成功日志 success: function(message) { console.log(`[成功] ${message}`); EducationHelper.Status?.setLastAction(message); }, // 警告日志 warn: function(message) { console.warn(`[警告] ${message}`); EducationHelper.Status?.setLastAction(`警告: ${message}`); }, // 错误日志 error: function(message, error) { if (error) { console.error(`[错误] ${message}`, error); } else { console.error(`[错误] ${message}`); } EducationHelper.Status?.setLastAction(`错误: ${message}`); } }, // 系统通知模块 - 封装 GM_notification Notification: { send: function(title, text, options = {}) { try { if (typeof GM_notification === 'function') { GM_notification({ title: '齐大教务助手 - ' + title, text: text, timeout: options.timeout || 5000, 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: 8000 }); }, }, // HTML 转义工具 - 防止 innerHTML 注入 escapeHtml: function(str) { if (typeof str !== 'string') return String(str); const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return str.replace(/[&<>"']/g, (c) => map[c]); }, // 运行时工具 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(); } } }, // 通用工具 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 * 5000), }; }) .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 || 8000; 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); }); } }, 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() { if (EducationHelper.Config.pageType.isCoursePage) { return `待选 ${EducationHelper.Config.state.targetCourses.length} / 已匹配 ${EducationHelper.Config.state.matchedCourses.length}`; } if (EducationHelper.Config.pageType.isEvaluationListPage) { const total = EducationHelper.EvaluationList?.progress?.total || 0; const completed = EducationHelper.EvaluationList?.progress?.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)}
`; } }, Preview: { highlightedTargets: new Set(), clear: function() { this.highlightedTargets.forEach(element => { if (element?.classList) { element.classList.remove('helper-preview-outline'); } }); this.highlightedTargets.clear(); }, highlightTargets: function(targets, description) { 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 (targetList[0]?.scrollIntoView) { try { targetList[0].scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch (error) { EducationHelper.Logger.debug('预览滚动失败', error); } } EducationHelper.UI.showMessage(`
演练模式
${description}
`, 'warning', 2200); } }, // UI模块 UI: { // 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?.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() { if (!this.elements.container || !this.elements.content) { return; } const viewportPadding = 20; const maxHeight = Math.max(320, window.innerHeight - viewportPadding * 2); const dragBarHeight = this.elements.dragBar?.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(); }; // 评估默认选项(A/B/C) 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 = '齐大教务助手_设置_' + new Date().toISOString().slice(0, 10) + '.json'; a.click(); URL.revokeObjectURL(url); EducationHelper.Logger.success('设置已导出'); EducationHelper.UI.showMessage('设置已导出为 JSON 文件', 'success', 2000); } catch (error) { EducationHelper.Logger.error('导出设置失败', error); EducationHelper.UI.showMessage('导出失败: ' + error.message, 'error', 3000); } }); } // 导入设置 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', 3000); } catch (error) { EducationHelper.Logger.error('导入设置失败', error); EducationHelper.UI.showMessage('导入失败: ' + error.message, 'error', 3000); } }; reader.readAsText(file); event.target.value = ''; }); } }, // UI样式 - iOS风格设计 styles: { // 将在初始化时添加 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; } `, // 添加CSS样式到页面 addStyles: function() { if (document.getElementById('qqhruHelperStyles')) { return; } const style = document.createElement('style'); style.id = 'qqhruHelperStyles'; style.textContent = this.cssRules; document.head.appendChild(style); } }, // 创建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; } // 添加样式 this.styles.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'; } // 标题和内容区域HTML 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('生成页面内容'); }, // 使UI可拖动 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 = 3000) { 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; } }, 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(); }, /** * 其他模块将在后续添加 */ // 抢课模块 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; }, // 更新课程列表UI 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; }, // 更新匹配成功课程UI 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), 1000 ); 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() { EducationHelper.Logger.action('开始检查课程...'); const iframeDoc = document.querySelector('#ifra')?.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', 2000); return; } const interval = Math.max(3, EducationHelper.Config.state.courseMonitorInterval) * 1000; this.roundCount = 0; EducationHelper.Config.state.courseMonitorEnabled = true; EducationHelper.Logger.action(`选课监控已启动,每 ${interval / 1000} 秒刷新一次`); 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() { this.roundCount++; const iframeDoc = document.querySelector('#ifra')?.contentDocument; if (!iframeDoc) { // 尝试刷新 iframe 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', 5000); // 自动执行抢课 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} 轮:未发现目标课程余量`); // 刷新 iframe 准备下一轮 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; } }, // 评估模块 - 处理单个评估页面 Evaluator: { // 选项选择功能 optionSelector: { getOptionCandidates: function() { return Array.from(document.querySelectorAll(BrowserHelperCore.selectors.evaluationChoices)) .map(element => { const parentText = EducationHelper.Utils.getElementText( element.closest('label, td, tr, li, div') ); const siblingText = EducationHelper.Utils.normalizeText( [ element.nextElementSibling?.textContent || '', element.parentElement?.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; // 1. 通过name属性查找 const mainTextarea = document.querySelector('textarea[name="zgpj"]'); if (mainTextarea) { if (EducationHelper.Utils.handleDryRun(mainTextarea, '已识别主观评价文本框,演练模式下不会真正填写')) { EducationHelper.Status.setRecognized('已识别主观评价文本框'); return true; } mainTextarea.value = content; // 触发change事件 const event = new Event('input', { bubbles: true }); mainTextarea.dispatchEvent(event); EducationHelper.Status.setRecognized('已填写主观评价文本框'); EducationHelper.Logger.success("已通过name='zgpj'找到并填写主观评价文本框"); filled = true; } // 2. 查找可见的文本区域 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; } } // 3. 兼容旧页面结构的 jQuery 兜底 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; } } // 4. 最后尝试任何文本区域 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 / 1000; // 确保倒计时显示元素存在且可见 this.ensureDisplayExists(); // 更新显示 this.updateDisplay(); // 开始倒计时 this.timer = EducationHelper.Runtime.setInterval(() => { this.seconds--; this.updateDisplay(); if (this.seconds <= 0) { this.stop(); if (typeof onComplete === 'function') { onComplete(); } } }, 1000); 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'; // 添加到UI容器中 const uiContent = document.getElementById('uiContent'); if (uiContent) { // 添加到UI内容区的合适位置 const buttons = uiContent.querySelector('div[style*="display: flex"]'); if (buttons) { uiContent.insertBefore(timerDisplay, buttons); } else { // 如果找不到按钮区域,添加到内容区最后 uiContent.appendChild(timerDisplay); } } else { // 如果找不到UI容器,添加到body 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() { 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 candidates[0]?.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); // 0表示不自动消失 } const submitButton = await EducationHelper.Utils.waitFor( () => this.findSubmitButton() || document.querySelector('#submit, #btnSubmit, .submit-btn, [name="submit"]'), { timeout: 4000, 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(); } }, 2000); } return true; } catch (error) { EducationHelper.Logger.error("尝试提交表单失败", error); } } if (statusMsg) { statusMsg.innerHTML = '
自动提交失败,请手动点击页面中的"提交"按钮
'; EducationHelper.Runtime.setTimeout(() => { if (statusMsg && document.contains(statusMsg)) { statusMsg.remove(); } }, 5000); } return false; }, // 处理确认对话框 handleConfirmDialog: async function(statusMsg) { EducationHelper.Logger.action("等待确认对话框..."); const confirmButton = await EducationHelper.Utils.waitFor( () => this.findConfirmButton(), { timeout: 3000, message: '未出现确认按钮' } ).catch(() => null); if (!confirmButton) { EducationHelper.Logger.action("未找到确认按钮,可能评价已直接提交或需要手动确认"); if (statusMsg) { statusMsg.innerHTML = '
可能需要手动确认提交,请检查是否有弹出确认窗口
'; EducationHelper.Runtime.setTimeout(() => { if (statusMsg && document.contains(statusMsg)) { statusMsg.remove(); } }, 5000); } 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(); } }, 2000); } EducationHelper.Logger.success("已完成评价提交"); EducationHelper.UI.showMessage(`
评价提交成功
可以继续处理下一个评估项。
`, 'success', 2000); 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?.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() { EducationHelper.Logger.action('开始评价流程...'); EducationHelper.Status.setNextAction('识别题项、填写评价内容'); await EducationHelper.Utils.waitFor( () => document.querySelector(BrowserHelperCore.selectors.evaluationInputs), { timeout: 10000, message: '评价页面元素未加载完成' } ).catch(error => { EducationHelper.Logger.warn(error.message); return null; }); // 显示状态消息 EducationHelper.UI.showMessage( '
正在进行评价操作...
', 'info', 2000 ); // 1. 选择选项 EducationHelper.Evaluator.optionSelector.selectByLetter( EducationHelper.Config.state.selectedOption ); if (EducationHelper.Config.state.dryRunMode) { EducationHelper.Evaluator.contentFiller.fillContent( document.getElementById('evaluationContent')?.value || EducationHelper.Config.content.evaluationComment ); EducationHelper.Status.setNextAction('关闭演练模式后可执行真实评价流程'); return; } // 2. 填写评价内容 EducationHelper.Runtime.setTimeout(() => { EducationHelper.Evaluator.contentFiller.fillContent( document.getElementById('evaluationContent')?.value || EducationHelper.Config.content.evaluationComment ); // 禁用开始按钮,防止重复点击 const startButton = document.getElementById('startEvaluation'); if (startButton) { startButton.disabled = true; startButton.style.opacity = '0.6'; startButton.textContent = '评价已开始'; } // 3. 如果自动提交已启用,启动倒计时 if (EducationHelper.Config.state.autoSubmitEnabled) { EducationHelper.Logger.action('已启用自动提交,开始倒计时...'); EducationHelper.Status.setNextAction('等待 120 秒后自动提交'); // 启动倒计时 EducationHelper.Evaluator.submitter.countdown.start( EducationHelper.Config.timers.autoSubmitDelay / 1000, // 倒计时结束回调 () => { if (EducationHelper.Config.state.autoSubmitEnabled) { EducationHelper.Logger.action('倒计时结束,自动提交评价'); // 提示用户即将提交 EducationHelper.UI.showMessage( `
倒计时结束
正在自动提交评价...
`, 'info', 2000 ); // 提交评价 EducationHelper.Runtime.setTimeout(() => { EducationHelper.Evaluator.process.submit(); }, 2000); } } ); } }, 500); }, // 提交评价 submit: async function() { EducationHelper.Logger.action('提交评价...'); EducationHelper.Status.setNextAction('识别提交按钮并尝试提交'); // 保存评价内容到配置 const contentElement = document.getElementById('evaluationContent'); if (contentElement) { EducationHelper.Config.content.evaluationComment = contentElement.value; // 保存到localStorage EducationHelper.Config.saveSettings(); } // 再次确认文本框已填写 EducationHelper.Evaluator.contentFiller.fillContent(); // 查找并点击提交按钮 await EducationHelper.Evaluator.submitter.findAndClickSubmitButton(); } }, // UI生成 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 previewBtn = document.getElementById('previewRandomTemplate'); if (previewBtn) { previewBtn.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', 2000); } }); // 初始化预览按钮显示状态 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(); }, 3000); } }); // 立即提交评价按钮 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(); // 检查localStorage中是否存在autoMode标记 if (EducationHelper.Config.state.autoMode) { EducationHelper.Logger.action('检测到全自动模式,自动开始评价流程'); EducationHelper.Runtime.setTimeout(() => { // 自动执行评价流程 this.process.start(); }, 1000); } return this; } }, // 评估列表模块 - 处理评估列表页面 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; // 在评估列表页,tbody 内的"查看"按钮代表已完成的评估条目 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; // 检测 label-success 元素(页面用 标记已完成) 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 = 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); } // 更新UI显示 this.updateUI(); return { total: totalCount, completed: completedCount }; } catch (error) { EducationHelper.Logger.error('统计评价进度时出错', error); return { total: 0, completed: 0 }; } }, // 更新进度显示UI 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/1000}秒后开始执行...`); this.stopCountdown(); // 设置初始倒计时值 const countdownElement = document.getElementById('countdownValue'); if (!countdownElement) return false; let secondsLeft = EducationHelper.Config.timers.autoClickEvaluationDelay / 1000; 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; } }, 2000); } 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'; } } } }, 1000); 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); } }, 1000); } else { EducationHelper.UI.showMessage('未找到评估按钮', 'warning', 3000); } }, // 安全点击按钮 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/1000} 秒后开始执行 `; } // 开始倒计时 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; } }, // UI生成 generateUI: function() { return ` ${EducationHelper.Status.generatePanel()}
评估进度
0/0
正在统计评价进度...
自动操作倒计时
${EducationHelper.Config.timers.autoClickEvaluationDelay/1000}
操作提示
自动点击默认关闭。全自动模式会依次进入待评项、填写内容并提交,操作过程中可随时取消。
`; }, // 事件绑定 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) { // 等待UI元素创建完成后启动倒计时 EducationHelper.Runtime.setTimeout(() => { const countdownDiv = document.getElementById('countdownDiv'); if (countdownDiv) { countdownDiv.style.display = 'block'; // 开始倒计时 this.autoClicker.startCountdown(); } }, 500); } return this; } }, // 绩点计算模块 - 解析成绩页表格计算 GPA 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.0, (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] === undefined); 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 !== undefined; rows.forEach(row => { const cells = row.querySelectorAll('td'); if (cells.length <= Math.max(...Object.values(colMap))) return; const courseId = colMap.courseId !== undefined ? cells[colMap.courseId].textContent.trim() : ''; const courseName = cells[colMap.courseName].textContent.trim(); const courseAttr = colMap.courseAttr !== undefined ? 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 = 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); // 降级提示 HTML 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)} ${c.excluded ? '-' : c.gpa.toFixed(1)}
`; }, bindEvents: function() { // 绩点页无交互按钮,纯展示 }, // 重新渲染 UI 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(); // 1 秒后重试 const self = this; EducationHelper.Runtime.setTimeout(function() { const retry1 = self.scrapeScores(); if (retry1.length > 0) { EducationHelper.Logger.success('延迟重试成功,找到 ' + retry1.length + ' 门课程'); self.refresh(); return; } // 2 秒后再试一次 EducationHelper.Runtime.setTimeout(function() { const retry2 = self.scrapeScores(); EducationHelper.Logger.action('第二次重试: ' + retry2.length + ' 门课程'); self.refresh(); }, 2000); }, 1000); } else { EducationHelper.Status.setNextAction('成绩数据已解析(' + firstScrape.length + ' 门)'); EducationHelper.Status.syncFromState(); } }, }, // 课表增强模块 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) { // 课程名:优先匹配 p-kcm 类,兜底取第一个非灰色段落 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()); // 教学楼/教室:优先匹配 p-jxl 类,兜底在灰色段中找含楼字的 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; // 剩余当教师 }); // 从父 td 的 id 解析星期和节次(兼容 id 格式 "weekday_period") const td = div.closest('td'); const tdId = 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; }, // 获取今天是星期几(1=周一 ... 7=周日) getTodayWeekday: function() { const d = 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 = 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() { // 课表页无交互按钮,纯展示 + 高亮 }, // 重新渲染 UI 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(); } }, }, // 首页仪表盘模块 Homepage: { // 快捷导航(使用 Config.urls 集中配置) _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: '', // 获取今天星期几(1=周一 ... 7=周日) _getTodayWeekday: function() { const d = 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: [] }; // 1. 从 infobox 提取概览统计(选择器来自 Config.selectors) 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(); } }); } // 2. 直接从整个 HTML 中提取课组节点(兼容各种格式) 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, }; }; // 方法1:从 zTree 的 DOM 节点中提取 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); }); } // 方法2(回退):从原始 HTML 文本中用正则提取 JSON name 字段 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; }, // 通过 fetch 获取课表页面,解析今日课程 _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'); // 复用 ScheduleEnhancer 的选择器策略 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: name, location: location, periods: periods, period: 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([]); }); }, // 生成今日课程 HTML _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(); }, }, // 培养方案完成度分析模块 PlanCompletion: { _degradeWarning: '', _data: null, // 从页面 DOM 抓取培养方案数据 scrapeData: function() { this._degradeWarning = ''; var data = { summary: {}, groups: [] }; // 1. 从 Tab#one 的 infobox 抓取总体概览(选择器来自 Config.selectors) 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(); } }); } // 2. 从 zTree 抓取课组完成情况(选择器来自 Config.selectors) 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: 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 = '⚠️ 未找到培养方案课组数据。页面可能尚未加载完成或结构已变更。'; } // 3. 通过 zTree API 收集每个未完成课组的缺课明细 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; // 按名称匹配 zTree 根节点 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'; // 总体数据(从 infobox 取) 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('初始化培养方案分析模块...'); // zTree 可能异步渲染,检查数据是否就绪 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.Config.init(); // 键盘快捷键注册 function registerKeyboardShortcuts() { document.addEventListener('keydown', function(e) { // Ctrl+Shift+H: 显示/隐藏面板 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(); } } } // Ctrl+Shift+S: 启停自动流程 if (e.ctrlKey && e.shiftKey && e.key === 'S') { e.preventDefault(); if (EducationHelper.Config.state.autoMode) { EducationHelper.shutdown('keyboard-shortcut'); EducationHelper.UI.showMessage('已通过快捷键停止自动流程', 'warning', 2000); } else { const startBtn = document.getElementById('startAutoMode'); if (startBtn) startBtn.click(); } } // Esc: 取消当前操作 if (e.key === 'Escape') { if (EducationHelper.Config.state.autoMode || EducationHelper.Config.state.autoClickEvaluationEnabled) { e.preventDefault(); EducationHelper.shutdown('escape-key'); EducationHelper.UI.showMessage('已通过 Esc 取消操作', 'warning', 2000); } } }); } // 版本更新检查(比对 @version 与 Greasyfork 页面) function checkForUpdate() { try { const currentVersion = '2.6.0'; const lastCheckKey = 'lastUpdateCheck'; const lastCheck = EducationHelper.Storage.get(lastCheckKey, 0); const now = Date.now(); // 每 24 小时最多检查一次 if (now - lastCheck < 86400000) return; EducationHelper.Storage.set(lastCheckKey, now); EducationHelper.Logger.debug('检查更新中...'); // 若未来发布到 Greasyfork 后可在此接入 API // 目前仅记录检查时间,版本更新依赖 @updateURL 自动机制 } catch (error) { EducationHelper.Logger.debug('更新检查跳过', error); } } // 主入口点 - 脚本初始化 function init() { console.log('齐大教务助手启动中...'); // 等待DOM加载完成 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initAfterDOMLoaded); } else { initAfterDOMLoaded(); } } // DOM加载完成后初始化 function initAfterDOMLoaded() { try { // 创建UI界面 EducationHelper.UI.create(); // 根据页面类型初始化相应模块 initModulesByPageType(); // 注册键盘快捷键 registerKeyboardShortcuts(); // 后台检查更新 checkForUpdate(); } catch (error) { console.error('[齐大教务助手] 初始化失败:', error); EducationHelper.Notification.scriptError('脚本初始化失败: ' + error.message); } } // 根据页面类型初始化模块 function initModulesByPageType() { const config = EducationHelper.Config; if (config.pageType.isCoursePage) { // 生成UI内容 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(); } // 启动脚本 init(); })();