// ==UserScript== // @name 齐大教务助手 // @namespace https://greasyfork.org/users/737539 // @version 2.6.0 // @description 集成抢课功能与教学评估功能,一体化教务助手 // @author 忘忧 // @icon https://xyh.qqhru.edu.cn/favicon.ico // @license MIT // @match http://111.43.36.164/student/teachingEvaluation/evaluation/index // @match http://111.43.36.164/student/courseSelect/courseSelect/index // @match http://111.43.36.164/student/teachingEvaluation/teachingEvaluation/evaluationPage // @match http://111.43.36.164/student/teachingEvaluation/teachingEvaluation/index // @match http://172.20.139.153:7700/student/teachingEvaluation/teachingEvaluation/evaluationPage // @match http://172.20.139.153:7700/student/teachingEvaluation/teachingEvaluation/index // @match https://172-20-139-153-7700.webvpn.qqhru.edu.cn/student/teachingEvaluation/teachingEvaluation/evaluationPage // @match https://172-20-139-153-7700.webvpn.qqhru.edu.cn/student/teachingEvaluation/teachingEvaluation/index // @match https://172-20-139-153-7700.webvpn.qqhru.edu.cn/student/teachingEvaluation/evaluation/index // @match http://172.20.139.153:7700/student/teachingEvaluation/evaluation/index // @match http://172.20.139.153:7700/student/courseSelect/courseSelect/index // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // ==/UserScript== (function () { 'use strict'; 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; /** * 齐大教务助手主模块 * 模块化结构,将不同功能划分为独立模块 */ const EducationHelper = { // 系统配置 Config: { // 全局状态 state: { targetCourses: [], matchedCourses: [], timer: null, autoMode: false, autoClickEvaluationEnabled: false, autoClickStopped: false, dryRunMode: false, selectedOption: "A", autoSubmitEnabled: true, debugMode: false, uiFollowPage: true, panelPosition: null, }, // 页面类型 pageType: { currentPageUrl: window.location.href, isCoursePage: false, isEvaluationPage: false, isEvaluationListPage: false }, // 时间设置 timers: { autoClickEvaluationDelay: 10000, // 延迟10秒执行自动点击 autoSubmitDelay: 120000, // 自动提交延迟,默认2分钟 }, // 内容设置 content: { evaluationComment: "上课有热情,积极解决学生问题,很好的老师!!", // 默认评价内容 }, // 初始化配置 init: function() { this.pageType.currentPageUrl = window.location.href; // 检测页面类型 this.pageType.isCoursePage = this.pageType.currentPageUrl.includes('courseSelect'); this.pageType.isEvaluationPage = this.pageType.currentPageUrl.includes('teachingEvaluation/evaluationPage'); this.pageType.isEvaluationListPage = this.pageType.currentPageUrl.includes('teachingEvaluation/teachingEvaluation/index') || this.pageType.currentPageUrl.includes('teachingEvaluation/evaluation/index'); // 从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, }); 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 (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}`); } }, // 运行时工具 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 '评估列表页'; } 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}`; } 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 }) => `
${label}
${value}
`).join('') + `
最近动作:${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'; }, // UI样式 - iOS风格设计 styles: { // 将在初始化时添加 cssRules: ` /* iOS风格全局样式 */ * { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } /* 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; } :root { --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; } 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; } } }, // 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(); }); }, // 初始化抢课模块 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: { // 填写评价内容 fillContent: function(content) { content = content || EducationHelper.Config.content.evaluationComment; 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 = $("#page-content-template > div > div > div.widget-content > form > div > table > tbody > tr:nth-child(25) > td > div > textarea"); 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('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) { const text = EducationHelper.Utils.getElementText(element); return !EducationHelper.Utils.isIgnoredEvaluationElement(element) && BrowserHelperCore.isEvaluationActionText(text); }, 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); return BrowserHelperCore.isCompletedText(text); }, 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)) .map(context => { const text = EducationHelper.Utils.getElementText(context); if (!text || (!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; this.total = totalCount; this.completed = completedCount; EducationHelper.Logger.debug(`进度统计: ${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(); }); // 调试模式复选框 document.getElementById('debugMode').addEventListener('change', function() { EducationHelper.Config.state.debugMode = this.checked; EducationHelper.Logger.action(`调试模式: ${this.checked ? '已启用' : '已禁用'}`); EducationHelper.Config.saveSettings(); // 显示/隐藏调试面板 const 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}`); EducationHelper.Logger.debug(`jQuery可用: ${typeof $ !== 'undefined'}`); } }); }, // 初始化 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; } }, }; // 初始化配置 EducationHelper.Config.init(); // 主入口点 - 脚本初始化 function init() { console.log('齐大教务助手启动中...'); // 等待DOM加载完成 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initAfterDOMLoaded); } else { initAfterDOMLoaded(); } } // DOM加载完成后初始化 function initAfterDOMLoaded() { // 创建UI界面 EducationHelper.UI.create(); // 根据页面类型初始化相应模块 initModulesByPageType(); } // 根据页面类型初始化模块 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 { // 不支持的页面 EducationHelper.UI.elements.content.innerHTML = `

当前页面不支持本助手功能

`; EducationHelper.UI.syncViewportConstraints(); } } // 启动脚本 init(); })();