// ==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()}
自动操作倒计时
${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 ? '✅ 使用教务系统官方绩点' : '⚠️ 公式估算(表中未找到绩点列)'}
计算说明
${usingOfficial
? '数据来源:直接读取成绩表中「绩点」列,与教务系统一致
'
: '算法:GPA = (百分制成绩 - 50) ÷ 10,上限 4.0(估算,可能与系统有差异)
'}
五级制:优秀→95, 良好→85, 中等→75, 及格→65
排除:「通过」「免修」等无绩点课程不计入
加权:按学分加权平均
全部课程明细(${courses.length}门)
| 课程 |
学分 |
成绩 |
绩点 |
${courses.map(c => `
| ${EducationHelper.escapeHtml(c.courseName)} |
${c.credit} |
${EducationHelper.escapeHtml(c.gradeText)} |
${c.excluded ? '-' : c.gpa.toFixed(1)} |
`).join('')}
`;
},
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 += '
';
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()}
`;
},
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();
})();