// ==UserScript==
// @name 齐大教务助手
// @namespace https://greasyfork.org/users/737539
// @version 2.6.0
// @description 集成抢课功能与教学评估功能,一体化教务助手
// @author 忘忧
// @icon https://xyh.qqhru.edu.cn/favicon.ico
// @license MIT
// @match http://111.43.36.164/student/teachingEvaluation/evaluation/index
// @match http://111.43.36.164/student/courseSelect/courseSelect/index
// @match http://111.43.36.164/student/teachingEvaluation/teachingEvaluation/evaluationPage
// @match http://111.43.36.164/student/teachingEvaluation/teachingEvaluation/index
// @match http://172.20.139.153:7700/student/teachingEvaluation/teachingEvaluation/evaluationPage
// @match http://172.20.139.153:7700/student/teachingEvaluation/teachingEvaluation/index
// @match https://172-20-139-153-7700.webvpn.qqhru.edu.cn/student/teachingEvaluation/teachingEvaluation/evaluationPage
// @match https://172-20-139-153-7700.webvpn.qqhru.edu.cn/student/teachingEvaluation/teachingEvaluation/index
// @match https://172-20-139-153-7700.webvpn.qqhru.edu.cn/student/teachingEvaluation/evaluation/index
// @match http://172.20.139.153:7700/student/teachingEvaluation/evaluation/index
// @match http://172.20.139.153:7700/student/courseSelect/courseSelect/index
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// ==/UserScript==
(function () {
'use strict';
const BrowserHelperCore = {
selectors: {
helperContainer: '#qqhruHelperUI',
helperStatusBody: '#helperStatusBody',
evaluationAction: 'button, a, input[type="button"], input[type="submit"]',
evaluationScope: '#page-content-template, #page-content, .page-content, .main-content, .widget-main, .widget-body, .widget-box, .tab-content, .content',
evaluationIgnore: '#qqhruHelperUI, #sidebar, .sidebar, .nav-list, .sidebar-menu, #breadcrumbs, .breadcrumbs, .breadcrumb, .page-header, .navbar, .ace-nav, .nav-tabs, .pagination, .footer',
evaluationContainer: 'tr, li, .list-item, .evaluation-item, .widget-main, .widget-body, .row',
evaluationInputs: 'textarea, .ace, input[type="radio"], input[type="checkbox"]',
evaluationChoices: 'input[type="radio"], input[type="checkbox"], .ace',
submitAction: 'button, input[type="submit"], input[type="button"], a.btn',
confirmAction: '.modal-footer .btn, .layui-layer-btn .layui-layer-btn0, .dialog-footer .btn, .layui-layer-btn a, button, a.btn, input[type="button"], input[type="submit"]',
backAction: 'a[href*="index"], button, a',
courseRows: 'tr',
courseCheckbox: 'input[type="checkbox"]',
},
normalizeText: function(text) {
return String(text || '').replace(/\s+/g, ' ').trim();
},
isEvaluationActionText: function(text) {
const normalizedText = this.normalizeText(text);
return (normalizedText.includes('评估') || normalizedText.includes('评价')) &&
!normalizedText.includes('统计') &&
!normalizedText.includes('提交') &&
!normalizedText.includes('保存');
},
isCompletedText: function(text) {
const normalizedText = this.normalizeText(text);
return /(已评|已完成|完成)/.test(normalizedText) &&
!/(未评|待评)/.test(normalizedText);
},
matchCourseTarget: function(target, rowText, numberTokens = []) {
const normalizedTarget = this.normalizeText(target);
const normalizedRowText = this.normalizeText(rowText);
if (!normalizedTarget) {
return false;
}
if (/^\d+$/.test(normalizedTarget)) {
return numberTokens.includes(normalizedTarget);
}
return normalizedRowText.includes(normalizedTarget);
},
scoreSubmitCandidate: function(candidate) {
const text = this.normalizeText(candidate.text);
const type = candidate.type || '';
const className = candidate.className || '';
const rectTop = candidate.rectTop || 0;
const viewportHeight = candidate.viewportHeight || 1000;
let score = 0;
if (text === '提交') {
score += 20;
}
if (text.includes('提交')) {
score += 10;
}
if (text.includes('保存并提交') || text.includes('确认提交')) {
score += 6;
}
if (type === 'submit') {
score += 6;
}
if (/btn-danger|btn-primary|layui-btn|submit/i.test(className)) {
score += 4;
}
if (rectTop > viewportHeight / 2) {
score += 3;
}
return score;
},
scoreBackCandidate: function(candidate) {
const text = this.normalizeText(candidate.text);
const href = candidate.href || '';
let score = 0;
if (text.includes('评估列表')) {
score += 14;
}
if (text.includes('返回')) {
score += 12;
}
if (text.includes('列表')) {
score += 10;
}
if (href.includes('teachingEvaluation')) {
score += 4;
}
if (href.includes('index')) {
score += 3;
}
return score;
},
pickBestBackCandidate: function(candidates = []) {
return candidates
.map(candidate => ({
candidate,
score: this.scoreBackCandidate(candidate),
}))
.sort((a, b) => b.score - a.score)[0]?.candidate || null;
}
};
const isNodeLikeEnv = typeof window === 'undefined' || typeof document === 'undefined';
if (typeof module !== 'undefined' && module.exports) {
module.exports = BrowserHelperCore;
}
if (isNodeLikeEnv) {
return;
}
const HELPER_INSTANCE_KEY = '__qqhruEducationHelperLoaded__';
if (window[HELPER_INSTANCE_KEY] || document.getElementById('qqhruHelperUI')) {
console.log('齐大教务助手已加载,跳过重复初始化。');
return;
}
window[HELPER_INSTANCE_KEY] = true;
/**
* 齐大教务助手主模块
* 模块化结构,将不同功能划分为独立模块
*/
const EducationHelper = {
// 系统配置
Config: {
// 全局状态
state: {
targetCourses: [],
matchedCourses: [],
timer: null,
autoMode: false,
autoClickEvaluationEnabled: false,
autoClickStopped: false,
dryRunMode: false,
selectedOption: "A",
autoSubmitEnabled: true,
debugMode: false,
uiFollowPage: true,
panelPosition: null,
},
// 页面类型
pageType: {
currentPageUrl: window.location.href,
isCoursePage: false,
isEvaluationPage: false,
isEvaluationListPage: false
},
// 时间设置
timers: {
autoClickEvaluationDelay: 10000, // 延迟10秒执行自动点击
autoSubmitDelay: 120000, // 自动提交延迟,默认2分钟
},
// 内容设置
content: {
evaluationComment: "上课有热情,积极解决学生问题,很好的老师!!", // 默认评价内容
},
// 初始化配置
init: function() {
this.pageType.currentPageUrl = window.location.href;
// 检测页面类型
this.pageType.isCoursePage = this.pageType.currentPageUrl.includes('courseSelect');
this.pageType.isEvaluationPage = this.pageType.currentPageUrl.includes('teachingEvaluation/evaluationPage');
this.pageType.isEvaluationListPage = this.pageType.currentPageUrl.includes('teachingEvaluation/teachingEvaluation/index') ||
this.pageType.currentPageUrl.includes('teachingEvaluation/evaluation/index');
// 从localStorage读取持久化设置
this.loadSavedSettings();
return this;
},
// 保存设置到localStorage
saveSettings: function() {
try {
EducationHelper.Storage.set('settings', {
autoMode: this.state.autoMode,
autoClickEvaluationEnabled: this.state.autoClickEvaluationEnabled,
autoSubmitEnabled: this.state.autoSubmitEnabled,
dryRunMode: this.state.dryRunMode,
selectedOption: this.state.selectedOption,
debugMode: this.state.debugMode,
uiFollowPage: this.state.uiFollowPage,
panelPosition: this.state.panelPosition,
evaluationComment: this.content.evaluationComment,
});
console.log('[设置] 已保存设置');
} catch (e) {
console.warn("[警告] 保存设置失败:", e);
}
},
// 从localStorage加载设置
loadSavedSettings: function() {
try {
const storedSettings = EducationHelper.Storage.get('settings', {});
if (storedSettings.autoMode === true) {
this.state.autoMode = true;
console.log('[检测] 从localStorage检测到全自动模式已启用');
}
if (typeof storedSettings.autoClickEvaluationEnabled === 'boolean') {
this.state.autoClickEvaluationEnabled = storedSettings.autoClickEvaluationEnabled;
}
if (typeof storedSettings.autoSubmitEnabled === 'boolean') {
this.state.autoSubmitEnabled = storedSettings.autoSubmitEnabled;
}
if (typeof storedSettings.dryRunMode === 'boolean') {
this.state.dryRunMode = storedSettings.dryRunMode;
}
if (typeof storedSettings.debugMode === 'boolean') {
this.state.debugMode = storedSettings.debugMode;
}
if (typeof storedSettings.uiFollowPage === 'boolean') {
this.state.uiFollowPage = storedSettings.uiFollowPage;
}
if (storedSettings.panelPosition) {
this.state.panelPosition = storedSettings.panelPosition;
}
if (storedSettings.evaluationComment) {
this.content.evaluationComment = storedSettings.evaluationComment;
}
if (storedSettings.selectedOption) {
this.state.selectedOption = storedSettings.selectedOption;
}
} catch (e) {
console.warn("[警告] 读取设置失败:", e);
}
}
},
// 存储模块
Storage: {
prefix: 'qqhru-helper:',
supportsGM: function() {
return typeof GM_getValue === 'function' && typeof GM_setValue === 'function';
},
getStorageKey: function(key) {
return `${this.prefix}${key}`;
},
get: function(key, fallback = null) {
const storageKey = this.getStorageKey(key);
try {
if (this.supportsGM()) {
const rawValue = GM_getValue(storageKey, null);
if (rawValue !== null && rawValue !== undefined) {
return JSON.parse(rawValue);
}
}
} catch (error) {
console.warn('[警告] 读取 GM 存储失败:', error);
}
try {
const rawValue = localStorage.getItem(storageKey);
if (rawValue !== null) {
return JSON.parse(rawValue);
}
} catch (error) {
console.warn('[警告] 读取 localStorage 失败:', error);
}
return fallback;
},
set: function(key, value) {
const storageKey = this.getStorageKey(key);
const serializedValue = JSON.stringify(value);
if (this.supportsGM()) {
GM_setValue(storageKey, serializedValue);
}
localStorage.setItem(storageKey, serializedValue);
},
remove: function(key) {
const storageKey = this.getStorageKey(key);
try {
if (typeof GM_deleteValue === 'function') {
GM_deleteValue(storageKey);
}
} catch (error) {
console.warn('[警告] 删除 GM 存储失败:', error);
}
localStorage.removeItem(storageKey);
}
},
// 日志模块
Logger: {
// 调试日志
debug: function(message, data) {
if (EducationHelper.Config.state.debugMode) {
console.log(`[调试] ${message}`, data || '');
const debugContent = document.getElementById('debugContent');
if (debugContent) {
const logItem = document.createElement('div');
logItem.style.borderBottom = '1px dashed #eee';
logItem.style.paddingBottom = '3px';
logItem.style.marginBottom = '3px';
let logText = message;
if (data) {
if (typeof data === 'object') {
try {
logText += ` ${JSON.stringify(data)}`;
} catch (e) {
logText += ` [复杂对象]`;
}
} else {
logText += ` ${data}`;
}
}
logItem.textContent = logText;
debugContent.appendChild(logItem);
debugContent.scrollTop = debugContent.scrollHeight; // 自动滚动到底部
}
}
},
// 信息日志
info: function(message) {
console.log(`[信息] ${message}`);
EducationHelper.Status?.setLastAction(message);
},
// 操作日志
action: function(message) {
console.log(`[操作] ${message}`);
EducationHelper.Status?.setLastAction(message);
},
// 成功日志
success: function(message) {
console.log(`[成功] ${message}`);
EducationHelper.Status?.setLastAction(message);
},
// 警告日志
warn: function(message) {
console.warn(`[警告] ${message}`);
EducationHelper.Status?.setLastAction(`警告: ${message}`);
},
// 错误日志
error: function(message, error) {
if (error) {
console.error(`[错误] ${message}`, error);
} else {
console.error(`[错误] ${message}`);
}
EducationHelper.Status?.setLastAction(`错误: ${message}`);
}
},
// 运行时工具
Runtime: {
setTimeout: function(callback, delay) {
return window.setTimeout(callback, delay);
},
clearTimeout: function(timerId) {
if (timerId) {
window.clearTimeout(timerId);
}
},
setInterval: function(callback, delay) {
return window.setInterval(callback, delay);
},
clearInterval: function(timerId) {
if (timerId) {
window.clearInterval(timerId);
}
},
observe: function(target, options, callback) {
if (!target || typeof MutationObserver === 'undefined') {
return null;
}
const observer = new MutationObserver(callback);
observer.observe(target, options);
return observer;
},
disconnectObserver: function(observer) {
if (observer) {
observer.disconnect();
}
}
},
// 通用工具
Utils: {
normalizeText: function(text) {
return BrowserHelperCore.normalizeText(text);
},
getElementText: function(element) {
if (!element) {
return '';
}
return this.normalizeText(
element.innerText ||
element.textContent ||
element.value ||
''
);
},
isIgnoredEvaluationElement: function(element) {
if (!element) {
return false;
}
return !!element.closest(BrowserHelperCore.selectors.evaluationIgnore);
},
getEvaluationSearchRoot: function() {
const defaultRoot = document.body || document.documentElement;
const candidates = Array.from(document.querySelectorAll(BrowserHelperCore.selectors.evaluationScope))
.filter(element => this.isElementVisible(element))
.filter(element => !this.isIgnoredEvaluationElement(element));
if (candidates.length === 0) {
return defaultRoot;
}
return candidates
.map(element => {
const rect = element.getBoundingClientRect();
return {
element,
score: (rect.width * rect.height) + (element.querySelectorAll(BrowserHelperCore.selectors.evaluationAction).length * 5000),
};
})
.sort((a, b) => b.score - a.score)[0].element;
},
hasEvaluationClosedNotice: function(root) {
const searchRoot = root || this.getEvaluationSearchRoot();
const alerts = Array.from(searchRoot.querySelectorAll('.alert, .alert-success, .alert-warning, .message, .no-data, .empty, .widget-body'));
return alerts.some(element =>
this.isElementVisible(element) &&
this.getElementText(element).includes('评估开关已关闭')
);
},
isElementVisible: function(element) {
if (!element || !element.isConnected) {
return false;
}
const style = window.getComputedStyle(element);
const rect = element.getBoundingClientRect();
return style.display !== 'none' &&
style.visibility !== 'hidden' &&
rect.width > 0 &&
rect.height > 0;
},
isElementDisabled: function(element) {
return !!(
!element ||
element.disabled ||
element.getAttribute('aria-disabled') === 'true' ||
element.classList.contains('disabled')
);
},
clickElement: function(element) {
if (!element) {
return false;
}
try {
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
} catch (error) {
// 忽略滚动失败
}
try {
element.click();
return true;
} catch (error) {
EducationHelper.Logger.warn('直接点击失败,尝试派发事件');
}
try {
const event = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window
});
element.dispatchEvent(event);
return true;
} catch (error) {
EducationHelper.Logger.error('点击元素失败', error);
return false;
}
},
findClickableByText: function(keywords, selectors = 'button, a, input[type="button"], input[type="submit"]') {
const normalizedKeywords = keywords.map(keyword => this.normalizeText(keyword));
const candidates = Array.from(document.querySelectorAll(selectors))
.filter(element => this.isElementVisible(element));
return candidates.find(element => {
const text = this.getElementText(element);
return normalizedKeywords.some(keyword => text.includes(keyword));
}) || null;
},
handleDryRun: function(targets, description) {
if (!EducationHelper.Config.state.dryRunMode) {
return false;
}
EducationHelper.Preview.highlightTargets(targets, description);
EducationHelper.Status.setNextAction('关闭演练模式后可执行真实点击/提交');
EducationHelper.Status.setLastAction(`演练模式: ${description}`);
return true;
},
waitFor: function(checker, options = {}) {
const timeout = options.timeout || 8000;
const interval = options.interval || 150;
const root = options.root || document.body || document.documentElement;
return new Promise((resolve, reject) => {
let settled = false;
let observer = null;
let intervalTimer = null;
let timeoutTimer = null;
const cleanup = () => {
EducationHelper.Runtime.disconnectObserver(observer);
EducationHelper.Runtime.clearInterval(intervalTimer);
EducationHelper.Runtime.clearTimeout(timeoutTimer);
};
const finish = (result) => {
if (settled) {
return;
}
settled = true;
cleanup();
resolve(result);
};
const fail = () => {
if (settled) {
return;
}
settled = true;
cleanup();
reject(new Error(options.message || '等待元素超时'));
};
const runCheck = () => {
try {
const result = checker();
if (result) {
finish(result);
}
} catch (error) {
EducationHelper.Logger.debug('waitFor 检查失败', error);
}
};
runCheck();
if (settled) {
return;
}
intervalTimer = EducationHelper.Runtime.setInterval(runCheck, interval);
timeoutTimer = EducationHelper.Runtime.setTimeout(fail, timeout);
observer = EducationHelper.Runtime.observe(root, {
childList: true,
subtree: true,
attributes: true
}, runCheck);
});
}
},
Status: {
state: {
nextAction: '等待用户操作',
lastAction: '脚本已就绪',
recognized: '等待识别',
},
generatePanel: function() {
return `
`;
},
getPageLabel: function() {
if (EducationHelper.Config.pageType.isCoursePage) {
return '抢课页';
}
if (EducationHelper.Config.pageType.isEvaluationPage) {
return '单个评估页';
}
if (EducationHelper.Config.pageType.isEvaluationListPage) {
return '评估列表页';
}
return '未知页面';
},
getModeLabel: function() {
if (EducationHelper.Config.state.autoMode) {
return '全自动';
}
if (EducationHelper.Config.state.autoClickEvaluationEnabled) {
return '自动点击评估';
}
return '手动';
},
getDryRunLabel: function() {
return EducationHelper.Config.state.dryRunMode ? '演练模式(不执行)' : '实际执行';
},
getModeSummary: function() {
return `${this.getModeLabel()} · ${this.getDryRunLabel()}`;
},
getWaitPolicy: function() {
if (EducationHelper.Config.pageType.isEvaluationPage || EducationHelper.Config.pageType.isEvaluationListPage) {
return '系统要求等待 120 秒,脚本不会跳过';
}
return '按页面实时操作,无强制等待';
},
getRecognizedSummary: function() {
if (EducationHelper.Config.pageType.isCoursePage) {
return `待选 ${EducationHelper.Config.state.targetCourses.length} / 已匹配 ${EducationHelper.Config.state.matchedCourses.length}`;
}
if (EducationHelper.Config.pageType.isEvaluationListPage) {
const total = EducationHelper.EvaluationList?.progress?.total || 0;
const completed = EducationHelper.EvaluationList?.progress?.completed || 0;
const pending = Math.max(total - completed, 0);
return `待评 ${pending} / 总数 ${total}`;
}
if (EducationHelper.Config.pageType.isEvaluationPage) {
const optionCount = document.querySelectorAll(BrowserHelperCore.selectors.evaluationChoices).length;
const textareaCount = document.querySelectorAll('textarea').length;
return `题项 ${optionCount} / 文本框 ${textareaCount}`;
}
return this.state.recognized;
},
setNextAction: function(value) {
this.state.nextAction = value;
this.render();
},
setLastAction: function(value) {
this.state.lastAction = value;
this.render();
},
setRecognized: function(value) {
this.state.recognized = value;
this.render();
},
syncFromState: function() {
this.render();
},
render: function() {
const container = document.querySelector(BrowserHelperCore.selectors.helperStatusBody);
if (!container) {
return;
}
const recognizedSummary = this.getRecognizedSummary() || this.state.recognized;
const rows = [
{ label: '页面', value: this.getPageLabel() },
{ label: '模式', value: this.getModeSummary() },
{ label: '识别', value: recognizedSummary, wide: true },
{ label: '下一步', value: this.state.nextAction, wide: true },
];
container.innerHTML = rows.map(({ label, value, wide }) => `
`).join('') + `
最近动作:${this.state.lastAction}
`;
}
},
Preview: {
highlightedTargets: new Set(),
clear: function() {
this.highlightedTargets.forEach(element => {
if (element?.classList) {
element.classList.remove('helper-preview-outline');
}
});
this.highlightedTargets.clear();
},
highlightTargets: function(targets, description) {
const targetList = (Array.isArray(targets) ? targets : [targets]).filter(Boolean);
this.clear();
targetList.forEach(target => {
if (target.classList) {
target.classList.add('helper-preview-outline');
}
this.highlightedTargets.add(target);
});
if (targetList[0]?.scrollIntoView) {
try {
targetList[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
} catch (error) {
EducationHelper.Logger.debug('预览滚动失败', error);
}
}
EducationHelper.UI.showMessage(`
演练模式
${description}
`, 'warning', 2200);
}
},
// UI模块
UI: {
// UI元素
elements: {
container: null,
dragBar: null,
content: null,
},
getDragBounds: function() {
const container = this.elements.container;
const dragBar = this.elements.dragBar;
if (!container) {
return {
minX: 0,
maxX: 0,
minY: 0,
maxY: 0,
};
}
const width = container.offsetWidth || 320;
const visibleWidth = Math.min(96, Math.max(64, Math.round(width * 0.35)));
const dragBarHeight = dragBar?.offsetHeight || 60;
return {
minX: Math.min(0, visibleWidth - width),
maxX: Math.max(0, window.innerWidth - visibleWidth),
minY: 0,
maxY: Math.max(0, window.innerHeight - dragBarHeight),
};
},
clampPanelPosition: function(left, top) {
const bounds = this.getDragBounds();
return {
left: Math.min(Math.max(left, bounds.minX), bounds.maxX),
top: Math.min(Math.max(top, bounds.minY), bounds.maxY),
};
},
applyPanelPosition: function(left, top) {
if (!this.elements.container) {
return { left, top };
}
const position = this.clampPanelPosition(left, top);
this.elements.container.style.left = `${Math.round(position.left)}px`;
this.elements.container.style.top = `${Math.round(position.top)}px`;
this.elements.container.style.right = 'auto';
return position;
},
syncViewportConstraints: function() {
if (!this.elements.container || !this.elements.content) {
return;
}
const viewportPadding = 20;
const maxHeight = Math.max(320, window.innerHeight - viewportPadding * 2);
const dragBarHeight = this.elements.dragBar?.offsetHeight || 60;
const contentMaxHeight = Math.max(180, maxHeight - dragBarHeight);
this.elements.container.style.maxHeight = `${Math.round(maxHeight)}px`;
this.elements.content.style.maxHeight = `${Math.round(contentMaxHeight)}px`;
this.elements.content.style.overflowY = 'auto';
this.elements.content.style.overscrollBehavior = 'contain';
this.elements.content.style.paddingBottom = '28px';
},
// UI样式 - iOS风格设计
styles: {
// 将在初始化时添加
cssRules: `
/* iOS风格全局样式 */
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* iOS风格按钮 */
.ui-button {
background: #2563EB;
color: white;
border: none;
padding: 12px 16px;
cursor: pointer;
width: 100%;
margin-bottom: 12px;
border-radius: 12px;
transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
font-weight: 600;
font-size: 16px;
letter-spacing: -0.24px;
box-shadow: 0 2px 8px rgba(0, 122, 255, 0.25);
position: relative;
overflow: hidden;
}
.ui-button::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparent;
border-radius: 12px;
pointer-events: none;
}
.ui-button:hover {
background: #1D4ED8;
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(0, 122, 255, 0.35);
}
.ui-button:active {
transform: scale(0.96);
box-shadow: 0 1px 4px rgba(0, 122, 255, 0.2);
}
.ui-button:disabled {
background: #E5E5EA;
color: #8E8E93;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* iOS风格输入框 */
.ui-input {
width: 100%;
padding: 12px 16px;
margin-bottom: 12px;
border: 1px solid #E5E5EA;
border-radius: 12px;
box-sizing: border-box;
font-size: 16px;
background: #FFFFFF;
transition: all 0.2s ease;
-webkit-appearance: none;
}
.ui-input:focus {
outline: none;
border-color: #007AFF;
box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1);
}
/* iOS风格标签 */
.ui-label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #1C1C1E;
font-size: 15px;
letter-spacing: -0.24px;
}
/* iOS风格面板 */
.ui-panel {
border: none;
border-radius: 16px;
padding: 16px;
margin-bottom: 16px;
background: #F2F2F7;
max-height: 140px;
overflow-y: auto;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.ui-panel-title {
font-weight: 700;
margin-bottom: 8px;
color: #007AFF;
font-size: 16px;
letter-spacing: -0.32px;
}
/* iOS风格标签页 */
.ui-tabs {
display: flex;
margin-bottom: 16px;
background: #F2F2F7;
border-radius: 12px;
padding: 4px;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
}
.ui-tab {
flex: 1;
padding: 10px 16px;
cursor: pointer;
background: transparent;
border: none;
border-radius: 8px;
margin: 0;
font-weight: 500;
color: #8E8E93;
transition: all 0.2s ease;
text-align: center;
font-size: 15px;
}
.ui-tab.active {
background: #FFFFFF;
color: #007AFF;
font-weight: 600;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.ui-tab-content {
display: none;
}
.ui-tab-content.active {
display: block;
}
/* iOS风格删除按钮 */
.delete-btn {
background: #FF3B30;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
padding: 6px 12px;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
}
.delete-btn:hover {
background: #D70015;
transform: scale(0.95);
}
/* 课程项目 */
.course-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #E5E5EA;
font-size: 15px;
}
.course-item:last-child {
border-bottom: none;
}
/* iOS风格复选框容器 */
.ui-checkbox-container {
display: flex;
align-items: center;
margin-bottom: 16px;
padding: 12px 16px;
background: #F2F2F7;
border-radius: 12px;
}
.ui-checkbox {
margin-right: 12px;
width: 20px;
height: 20px;
accent-color: #007AFF;
}
/* 选项容器 */
.option-container {
margin-bottom: 16px;
background: #F2F2F7;
border-radius: 12px;
padding: 8px;
}
.option-row {
margin-bottom: 0;
}
.radio-label {
display: flex;
align-items: center;
cursor: pointer;
padding: 12px 16px;
border-radius: 8px;
transition: background-color 0.2s ease;
font-size: 15px;
font-weight: 500;
}
.radio-label:hover {
background: rgba(0, 122, 255, 0.1);
}
.radio-label input {
margin-right: 12px;
width: 18px;
height: 18px;
accent-color: #007AFF;
}
/* iOS风格计时器显示 */
.timer-display {
background: #EFF6FF;
color: #2563EB;
padding: 12px 16px;
border-radius: 12px;
text-align: center;
margin-bottom: 16px;
font-weight: 600;
font-size: 16px;
display: none;
box-shadow: 0 2px 8px rgba(52, 199, 89, 0.25);
}
.helper-preview-outline {
outline: 3px solid #FF9500 !important;
outline-offset: 3px;
box-shadow: 0 0 0 6px rgba(255, 149, 0, 0.18) !important;
transition: box-shadow 0.2s ease, outline-color 0.2s ease;
}
.helper-status-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.helper-status-row {
display: block;
padding: 8px 10px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(36, 91, 145, 0.08);
}
.helper-status-row.wide {
grid-column: 1 / -1;
}
.helper-status-label {
display: block;
margin-bottom: 4px;
font-size: 11px;
font-weight: 700;
color: #245B91;
}
.helper-status-value {
font-size: 12px;
line-height: 1.5;
color: #1C1C1E;
word-break: break-word;
}
:root {
--helper-bg: #ffffff;
--helper-surface: #f8fafc;
--helper-surface-soft: #f1f5f9;
--helper-border: #dbe3ec;
--helper-text: #172334;
--helper-muted: #66758a;
--helper-primary: #2563eb;
--helper-primary-hover: #1d4ed8;
--helper-primary-soft: #eff6ff;
--helper-warning-bg: #fff7ed;
--helper-warning-border: #fed7aa;
--helper-warning-text: #c2410c;
--helper-success-bg: #f0fdf4;
--helper-success-border: #bbf7d0;
--helper-success-text: #166534;
--helper-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
}
#qqhruHelperUI {
width: 300px !important;
background: var(--helper-bg) !important;
border: 1px solid var(--helper-border) !important;
border-radius: 14px !important;
box-shadow: var(--helper-shadow) !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif !important;
}
#dragBar {
background: var(--helper-bg) !important;
color: var(--helper-text) !important;
border-bottom: 1px solid var(--helper-border) !important;
padding: 12px 14px !important;
}
#dragBar span {
font-size: 15px !important;
font-weight: 700 !important;
letter-spacing: 0 !important;
}
#dragBar button {
background: var(--helper-surface) !important;
color: var(--helper-text) !important;
border: 1px solid var(--helper-border) !important;
border-radius: 10px !important;
box-shadow: none !important;
}
#dragBar button:hover {
background: var(--helper-surface-soft) !important;
}
#closeBtn {
background: #fff1f2 !important;
color: #dc2626 !important;
border-color: #fecdd3 !important;
}
#closeBtn:hover {
background: #ffe4e6 !important;
}
#uiContent {
background: var(--helper-bg) !important;
padding: 14px !important;
}
.ui-tabs {
background: transparent !important;
padding: 0 !important;
box-shadow: none !important;
margin-bottom: 12px !important;
}
.ui-tab {
background: var(--helper-surface) !important;
color: var(--helper-muted) !important;
border: 1px solid var(--helper-border) !important;
border-radius: 10px !important;
box-shadow: none !important;
font-weight: 600 !important;
padding: 8px 12px !important;
font-size: 14px !important;
}
.ui-tab.active {
background: var(--helper-primary-soft) !important;
color: var(--helper-primary) !important;
border-color: #cfe0ff !important;
}
.ui-button {
background: var(--helper-primary) !important;
color: #ffffff !important;
border: 1px solid transparent !important;
border-radius: 10px !important;
box-shadow: none !important;
padding: 10px 14px !important;
font-size: 14px !important;
letter-spacing: 0 !important;
}
.ui-button:hover {
background: var(--helper-primary-hover) !important;
transform: none !important;
box-shadow: none !important;
}
.ui-button::before {
display: none !important;
}
.ui-button:active {
transform: none !important;
box-shadow: none !important;
}
.ui-button:disabled,
#stopScript {
background: var(--helper-surface) !important;
color: var(--helper-muted) !important;
border: 1px solid var(--helper-border) !important;
box-shadow: none !important;
}
.ui-input,
textarea.ui-input {
border: 1px solid var(--helper-border) !important;
border-radius: 10px !important;
background: var(--helper-bg) !important;
box-shadow: none !important;
font-size: 14px !important;
}
.ui-label {
margin-bottom: 6px !important;
font-size: 13px !important;
font-weight: 600 !important;
color: var(--helper-text) !important;
letter-spacing: 0 !important;
}
.ui-panel,
.ui-checkbox-container,
.option-container {
background: var(--helper-surface) !important;
border: 1px solid var(--helper-border) !important;
border-radius: 12px !important;
box-shadow: none !important;
}
.ui-panel {
padding: 10px !important;
margin-bottom: 10px !important;
}
.ui-checkbox-container {
display: grid !important;
grid-template-columns: 18px 1fr !important;
column-gap: 10px !important;
align-items: center !important;
padding: 9px 10px !important;
}
.ui-checkbox-container.compact {
margin-bottom: 0 !important;
padding: 8px 10px !important;
}
.ui-checkbox {
width: 18px !important;
height: 18px !important;
margin: 0 !important;
vertical-align: middle !important;
accent-color: var(--helper-primary) !important;
align-self: center !important;
justify-self: start !important;
}
.ui-checkbox-container .ui-label {
display: block !important;
margin: 0 !important;
font-size: 14px !important;
line-height: 1.45 !important;
color: var(--helper-text) !important;
cursor: pointer !important;
}
.option-container {
padding: 4px !important;
}
.ui-panel-title {
color: var(--helper-text) !important;
font-size: 14px !important;
margin-bottom: 8px !important;
letter-spacing: 0 !important;
}
.radio-label:hover {
background: var(--helper-primary-soft) !important;
}
.helper-status-row {
background: var(--helper-surface) !important;
border-color: var(--helper-border) !important;
padding: 7px 9px !important;
border-radius: 10px !important;
}
.helper-status-label {
font-size: 12px !important;
color: var(--helper-muted) !important;
}
.helper-status-value {
font-size: 12px !important;
color: var(--helper-text) !important;
}
.helper-status-note {
margin-top: 2px !important;
grid-column: 1 / -1 !important;
padding: 6px 2px 0 !important;
font-size: 12px !important;
line-height: 1.5 !important;
color: var(--helper-muted) !important;
}
.ui-disclosure {
margin-bottom: 10px !important;
border: 1px solid var(--helper-border) !important;
border-radius: 12px !important;
background: var(--helper-surface) !important;
overflow: hidden !important;
}
.ui-disclosure summary {
list-style: none !important;
cursor: pointer !important;
padding: 10px 12px !important;
font-size: 13px !important;
font-weight: 600 !important;
color: var(--helper-text) !important;
}
.ui-disclosure summary::-webkit-details-marker {
display: none !important;
}
.ui-disclosure[open] summary {
border-bottom: 1px solid var(--helper-border) !important;
}
.ui-disclosure-body {
padding: 10px 12px !important;
font-size: 12px !important;
line-height: 1.6 !important;
color: var(--helper-muted) !important;
}
.timer-display,
#countdownDiv {
background: var(--helper-primary-soft) !important;
color: var(--helper-primary) !important;
border: 1px solid #cfe0ff !important;
border-radius: 10px !important;
box-shadow: none !important;
}
#qqhruHelperUI .ui-panel[style*="#FFF8E1"] {
background: var(--helper-warning-bg) !important;
border-color: var(--helper-warning-border) !important;
}
#qqhruHelperUI .ui-panel[style*="#FFF8E1"] .ui-panel-title,
#qqhruHelperUI .ui-panel[style*="#FFF8E1"] div {
color: var(--helper-warning-text) !important;
}
#evaluationProgress,
#debugInfo {
background: var(--helper-surface) !important;
}
`,
// 添加CSS样式到页面
addStyles: function() {
if (document.getElementById('qqhruHelperStyles')) {
return;
}
const style = document.createElement('style');
style.id = 'qqhruHelperStyles';
style.textContent = this.cssRules;
document.head.appendChild(style);
}
},
// 创建UI界面
create: function() {
EducationHelper.Logger.action('正在创建助手面板...');
if (document.getElementById('qqhruHelperUI')) {
this.elements.container = document.getElementById('qqhruHelperUI');
this.elements.dragBar = document.getElementById('dragBar');
this.elements.content = document.getElementById('uiContent');
return this;
}
// 添加样式
this.styles.addStyles();
// 创建主容器
this.elements.container = document.createElement('div');
this.elements.container.id = 'qqhruHelperUI';
this.elements.container.style.position = EducationHelper.Config.state.uiFollowPage ? 'fixed' : 'absolute';
this.elements.container.style.top = '20px';
this.elements.container.style.right = '20px';
this.elements.container.style.width = '300px';
this.elements.container.style.backgroundColor = '#FFFFFF';
this.elements.container.style.border = '1px solid #DBE3EC';
this.elements.container.style.padding = '0';
this.elements.container.style.zIndex = '9999';
this.elements.container.style.boxShadow = '0 10px 24px rgba(15, 23, 42, 0.08)';
this.elements.container.style.borderRadius = '14px';
this.elements.container.style.fontFamily = '"Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif';
this.elements.container.style.backdropFilter = 'none';
this.elements.container.style.webkitBackdropFilter = 'none';
this.elements.container.style.overflow = 'hidden';
if (EducationHelper.Config.state.panelPosition) {
this.elements.container.style.left = `${EducationHelper.Config.state.panelPosition.left}px`;
this.elements.container.style.top = `${EducationHelper.Config.state.panelPosition.top}px`;
this.elements.container.style.right = 'auto';
}
// 标题和内容区域HTML
this.elements.container.innerHTML = `
`;
document.body.appendChild(this.elements.container);
// 保存元素引用
this.elements.dragBar = document.getElementById('dragBar');
this.elements.content = document.getElementById('uiContent');
this.syncViewportConstraints();
if (EducationHelper.Config.state.panelPosition) {
this.applyPanelPosition(
EducationHelper.Config.state.panelPosition.left,
EducationHelper.Config.state.panelPosition.top
);
}
// 根据页面类型生成内容
this.generateContent();
// 实现拖动功能
this.makeDraggable();
// 添加按钮事件
document.getElementById('minimizeBtn').addEventListener('click', this.toggleMinimize.bind(this));
document.getElementById('closeBtn').addEventListener('click', () => {
EducationHelper.shutdown('用户关闭面板');
// 关闭动画
this.elements.container.style.transform = 'scale(0.8)';
this.elements.container.style.opacity = '0';
EducationHelper.Runtime.setTimeout(() => {
this.elements.container.style.display = 'none';
this.elements.container.style.transform = 'scale(1)';
this.elements.container.style.opacity = '1';
}, 200);
});
EducationHelper.Logger.success('助手面板创建完成');
return this;
},
// 根据页面类型生成内容
generateContent: function() {
// 将在后续实现
EducationHelper.Logger.action('生成页面内容');
},
// 使UI可拖动
makeDraggable: function() {
let offsetX = 0;
let offsetY = 0;
let isDragging = false;
this.elements.dragBar.addEventListener('mousedown', (e) => {
isDragging = true;
this.elements.container.style.right = 'auto';
offsetX = e.clientX - this.elements.container.getBoundingClientRect().left;
offsetY = e.clientY - this.elements.container.getBoundingClientRect().top;
this.elements.dragBar.style.cursor = 'grabbing';
document.body.style.userSelect = 'none';
});
document.addEventListener('mousemove', (e) => {
if (isDragging) {
const newX = e.clientX - offsetX;
const newY = e.clientY - offsetY;
this.applyPanelPosition(newX, newY);
}
});
document.addEventListener('mouseup', () => {
isDragging = false;
this.elements.dragBar.style.cursor = 'grab';
document.body.style.userSelect = '';
const rect = this.elements.container.getBoundingClientRect();
EducationHelper.Config.state.panelPosition = this.clampPanelPosition(rect.left, rect.top);
EducationHelper.Config.saveSettings();
});
window.addEventListener('resize', () => {
if (!this.elements.container || this.elements.container.style.display === 'none') {
return;
}
this.syncViewportConstraints();
const rect = this.elements.container.getBoundingClientRect();
const position = this.applyPanelPosition(rect.left, rect.top);
EducationHelper.Config.state.panelPosition = {
left: Math.round(position.left),
top: Math.round(position.top),
};
EducationHelper.Config.saveSettings();
});
},
// 切换最小化状态
toggleMinimize: function() {
const content = this.elements.content;
const btn = document.getElementById('minimizeBtn');
if (content.style.display === 'none') {
// 展开动画
content.style.display = 'block';
this.syncViewportConstraints();
content.style.opacity = '0';
content.style.transform = 'translateY(-10px)';
EducationHelper.Runtime.setTimeout(() => {
content.style.transition = 'all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
content.style.opacity = '1';
content.style.transform = 'translateY(0)';
}, 10);
btn.textContent = '−';
btn.style.transform = 'rotate(0deg)';
} else {
// 收起动画
content.style.transition = 'all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
content.style.opacity = '0';
content.style.transform = 'translateY(-10px)';
EducationHelper.Runtime.setTimeout(() => {
content.style.display = 'none';
content.style.transition = '';
}, 300);
btn.textContent = '+';
btn.style.transform = 'rotate(90deg)';
}
},
// 显示状态消息
showMessage: function(message, type = 'info', duration = 3000) {
const msgElement = document.createElement('div');
msgElement.style = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.96);
padding: 14px 16px;
border-radius: 12px;
z-index: 10000;
text-align: center;
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12);
color: #172334;
font-weight: 600;
font-size: 14px;
line-height: 1.5;
opacity: 0;
transition: all 0.2s ease;
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
max-width: 280px;
min-width: 180px;
border: 1px solid #DBE3EC;
background: #FFFFFF;
`;
// 根据类型设置颜色
switch(type) {
case 'success':
msgElement.style.background = '#F0FDF4';
msgElement.style.borderColor = '#BBF7D0';
msgElement.style.color = '#166534';
break;
case 'error':
msgElement.style.background = '#FEF2F2';
msgElement.style.borderColor = '#FECACA';
msgElement.style.color = '#B91C1C';
break;
case 'warning':
msgElement.style.background = '#FFF7ED';
msgElement.style.borderColor = '#FED7AA';
msgElement.style.color = '#C2410C';
break;
default:
msgElement.style.background = '#EFF6FF';
msgElement.style.borderColor = '#BFDBFE';
msgElement.style.color = '#1D4ED8';
}
msgElement.innerHTML = message;
document.body.appendChild(msgElement);
EducationHelper.Runtime.setTimeout(() => {
msgElement.style.opacity = '1';
msgElement.style.transform = 'translate(-50%, -50%) scale(1)';
}, 10);
// 自动消失
if (duration > 0) {
EducationHelper.Runtime.setTimeout(() => {
if (msgElement && document.contains(msgElement)) {
msgElement.style.opacity = '0';
msgElement.style.transform = 'translate(-50%, -50%) scale(0.96)';
EducationHelper.Runtime.setTimeout(() => {
if (msgElement && document.contains(msgElement)) {
msgElement.remove();
}
}, 300);
}
}, duration);
}
return msgElement;
}
},
shutdown: function(reason = 'manual-stop', options = {}) {
EducationHelper.Logger.action(`停止自动流程: ${reason}`);
const keepProgressTracking = options.keepProgressTracking === true;
EducationHelper.Config.state.autoMode = false;
EducationHelper.Config.state.autoClickEvaluationEnabled = false;
EducationHelper.Config.state.autoClickStopped = true;
if (EducationHelper.Config.state.timer) {
EducationHelper.Runtime.clearInterval(EducationHelper.Config.state.timer);
EducationHelper.Config.state.timer = null;
}
try {
EducationHelper.Evaluator.submitter.countdown.stop();
} catch (error) {
EducationHelper.Logger.debug('停止评价倒计时时忽略异常', error);
}
try {
EducationHelper.EvaluationList.autoClicker.stopCountdown();
EducationHelper.EvaluationList.scanner.stopPendingClick();
if (!keepProgressTracking) {
EducationHelper.EvaluationList.progress.stopTracking();
}
} catch (error) {
EducationHelper.Logger.debug('停止评估列表自动化时忽略异常', error);
}
const startButton = document.getElementById('startScript');
const stopButton = document.getElementById('stopScript');
if (startButton) {
startButton.disabled = false;
}
if (stopButton) {
stopButton.disabled = true;
}
const autoClickCheckbox = document.getElementById('autoClickEvaluation');
if (autoClickCheckbox) {
autoClickCheckbox.checked = false;
}
EducationHelper.Preview.clear();
EducationHelper.Status.setNextAction('等待用户重新开始');
EducationHelper.Config.saveSettings();
},
/**
* 其他模块将在后续添加
*/
// 抢课模块
CourseGrabber: {
// 课程列表操作
courseList: {
// 添加课程
add: function(courseCode) {
if (!courseCode) return false;
const courses = courseCode.split('\n').map(code => code.trim());
courses.forEach(course => {
if (course && !EducationHelper.Config.state.targetCourses.includes(course)) {
EducationHelper.Config.state.targetCourses.push(course);
}
});
this.updateUI();
EducationHelper.Status.syncFromState();
EducationHelper.Logger.success(`已添加课程: ${courses.join(', ')}`);
return true;
},
// 移除课程
remove: function(index) {
if (index >= 0 && index < EducationHelper.Config.state.targetCourses.length) {
const removedCourse = EducationHelper.Config.state.targetCourses.splice(index, 1)[0];
this.updateUI();
EducationHelper.Status.render();
EducationHelper.Status.syncFromState();
EducationHelper.Logger.success(`已移除课程: ${removedCourse}`);
return true;
}
return false;
},
// 更新课程列表UI
updateUI: function() {
const courseListDiv = document.getElementById('courseList');
if (!courseListDiv) return;
courseListDiv.innerHTML = '';
if (EducationHelper.Config.state.targetCourses.length === 0) {
courseListDiv.innerHTML = `
暂无课程,请先添加课程代码
`;
} else {
EducationHelper.Config.state.targetCourses.forEach((course, index) => {
const courseItem = document.createElement('div');
courseItem.className = 'course-item';
courseItem.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #E5E5EA;
font-size: 15px;
font-weight: 500;
`;
courseItem.innerHTML = `
${course}
`;
// 最后一个项目不显示底部边框
if (index === EducationHelper.Config.state.targetCourses.length - 1) {
courseItem.style.borderBottom = 'none';
}
courseListDiv.appendChild(courseItem);
});
// 添加删除按钮事件
courseListDiv.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const idx = parseInt(e.target.getAttribute('data-index'));
this.remove(idx);
});
// 添加悬停效果
btn.addEventListener('mouseenter', function() {
this.style.background = '#FFE4E6';
});
btn.addEventListener('mouseleave', function() {
this.style.background = '#FFF1F2';
});
});
}
}
},
// 匹配成功课程操作
matchedCourses: {
// 添加匹配成功的课程
add: function(courseCode) {
if (courseCode && !EducationHelper.Config.state.matchedCourses.includes(courseCode)) {
EducationHelper.Config.state.matchedCourses.push(courseCode);
this.updateUI();
EducationHelper.Logger.success(`已添加匹配成功课程: ${courseCode}`);
return true;
}
return false;
},
// 更新匹配成功课程UI
updateUI: function() {
const matchedCoursesDiv = document.getElementById('matchedCourses');
if (!matchedCoursesDiv) return;
matchedCoursesDiv.innerHTML = '';
if (EducationHelper.Config.state.matchedCourses.length === 0) {
matchedCoursesDiv.innerHTML = `
暂无匹配成功的课程
`;
} else {
EducationHelper.Config.state.matchedCourses.forEach((course, index) => {
const courseItem = document.createElement('div');
courseItem.className = 'course-item';
courseItem.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #E5E5EA;
font-size: 15px;
font-weight: 500;
`;
courseItem.innerHTML = `
已匹配
${course}
`;
// 最后一个项目不显示底部边框
if (index === EducationHelper.Config.state.matchedCourses.length - 1) {
courseItem.style.borderBottom = 'none';
}
matchedCoursesDiv.appendChild(courseItem);
});
}
}
},
// 抢课过程控制
control: {
getCourseRows: function(iframeDoc) {
return Array.from(iframeDoc.querySelectorAll(BrowserHelperCore.selectors.courseRows))
.map(row => {
const checkbox = row.querySelector(BrowserHelperCore.selectors.courseCheckbox);
if (!checkbox) {
return null;
}
const text = EducationHelper.Utils.getElementText(row);
const numberTokens = text.match(/\d+/g) || [];
return {
row,
checkbox,
text,
numberTokens,
};
})
.filter(Boolean);
},
matchCourseRow: function(courseRow, targetCourse) {
return BrowserHelperCore.matchCourseTarget(
targetCourse,
courseRow.text,
courseRow.numberTokens
);
},
// 启动抢课
start: function() {
if (EducationHelper.Config.state.targetCourses.length === 0) {
EducationHelper.UI.showMessage('请先添加课程', 'error');
return false;
}
if (EducationHelper.Config.state.timer) {
EducationHelper.Logger.warn('抢课脚本已经在运行中');
return true;
}
// 设置按钮状态
document.getElementById('startScript').disabled = true;
document.getElementById('stopScript').disabled = false;
EducationHelper.Status.setNextAction('扫描课程列表并匹配目标课程');
if (EducationHelper.Config.state.dryRunMode) {
EducationHelper.Logger.action('演练模式下执行一次识别,不会进行实际勾选或提交');
this.checkAndSelectCourses();
document.getElementById('startScript').disabled = false;
document.getElementById('stopScript').disabled = true;
return true;
}
// 启动定时器
EducationHelper.Config.state.timer = EducationHelper.Runtime.setInterval(
this.checkAndSelectCourses.bind(this),
1000
);
EducationHelper.Logger.action('抢课脚本已启动');
this.checkAndSelectCourses();
return true;
},
// 停止抢课
stop: function() {
if (EducationHelper.Config.state.timer) {
EducationHelper.Runtime.clearInterval(EducationHelper.Config.state.timer);
EducationHelper.Config.state.timer = null;
// 设置按钮状态
document.getElementById('startScript').disabled = false;
document.getElementById('stopScript').disabled = true;
EducationHelper.Status.setNextAction('等待用户重新开始');
EducationHelper.Logger.action('抢课脚本已停止');
return true;
}
return false;
},
// 检查并选择课程
checkAndSelectCourses: function() {
EducationHelper.Logger.action('开始检查课程...');
const iframeDoc = document.querySelector('#ifra')?.contentDocument;
if (!iframeDoc) {
EducationHelper.Logger.error('无法获取 iframe 文档');
return;
}
if (EducationHelper.Config.state.targetCourses.length === 0) {
EducationHelper.Logger.action('所有课程已处理,尝试提交...');
this.clickSubmitButton();
this.stop();
return;
}
const courseRows = this.getCourseRows(iframeDoc);
EducationHelper.Logger.debug(`本轮共找到 ${courseRows.length} 条可选课程记录`);
EducationHelper.Status.setRecognized(`识别到 ${courseRows.length} 条课程记录`);
const matchedCourses = [];
const unmatchedCourses = [];
const matchedTargets = [];
EducationHelper.Config.state.targetCourses.forEach(targetCourse => {
const matchedRow = courseRows.find(courseRow => this.matchCourseRow(courseRow, targetCourse));
if (!matchedRow) {
unmatchedCourses.push(targetCourse);
return;
}
matchedTargets.push(matchedRow.checkbox);
if (EducationHelper.Config.state.dryRunMode) {
matchedCourses.push(targetCourse);
return;
}
if (!matchedRow.checkbox.checked) {
EducationHelper.Utils.clickElement(matchedRow.checkbox);
EducationHelper.Logger.success(`已勾选课程: ${targetCourse}`);
}
matchedCourses.push(targetCourse);
EducationHelper.CourseGrabber.matchedCourses.add(targetCourse);
});
if (EducationHelper.Config.state.dryRunMode) {
if (matchedTargets.length > 0) {
EducationHelper.Utils.handleDryRun(
matchedTargets,
`已识别 ${matchedTargets.length} 个可勾选课程,演练模式下不会真正勾选`
);
EducationHelper.Status.setRecognized(`演练命中 ${matchedCourses.length} / 目标 ${EducationHelper.Config.state.targetCourses.length}`);
} else {
EducationHelper.Status.setRecognized(`演练未命中,目标 ${EducationHelper.Config.state.targetCourses.length}`);
}
return;
}
EducationHelper.Config.state.targetCourses = unmatchedCourses;
EducationHelper.CourseGrabber.courseList.updateUI();
EducationHelper.Status.syncFromState();
matchedCourses.forEach(course => {
EducationHelper.Logger.action(`本轮已处理课程: ${course}`);
});
if (matchedCourses.length === 0) {
EducationHelper.Logger.warn(`本轮未匹配到目标课程: ${EducationHelper.Config.state.targetCourses.join(', ')}`);
}
if (EducationHelper.Config.state.targetCourses.length === 0 && matchedCourses.length > 0) {
EducationHelper.Logger.action('目标课程已全部处理,尝试提交...');
this.clickSubmitButton();
this.stop();
}
},
// 点击提交按钮
clickSubmitButton: function() {
const button = document.querySelector('#submitButton');
if (button) {
EducationHelper.Logger.action('找到提交按钮,正在尝试提交...');
if (EducationHelper.Utils.handleDryRun(button, '已识别提交按钮,演练模式下不会真正提交选课')) {
return true;
}
EducationHelper.Utils.clickElement(button);
EducationHelper.Logger.success('已提交选课请求');
return true;
} else {
EducationHelper.Logger.warn('未找到提交按钮,请检查页面结构');
return false;
}
}
},
// UI内容生成
generateUI: function() {
return `
${EducationHelper.Status.generatePanel()}
操作提示
演练模式只会高亮识别结果。关闭后才会真正勾选并提交课程。
`;
},
// 注册事件监听
bindEvents: function() {
document.getElementById('courseDryRunMode').addEventListener('change', function() {
EducationHelper.Config.state.dryRunMode = this.checked;
EducationHelper.Config.saveSettings();
EducationHelper.Status.syncFromState();
EducationHelper.Logger.action(`抢课演练模式: ${this.checked ? '已启用' : '已关闭'}`);
});
document.getElementById('addCourses').addEventListener('click', () => {
const courseCodesInput = document.getElementById('courseCode').value.trim();
if (this.courseList.add(courseCodesInput)) {
document.getElementById('courseCode').value = '';
}
});
document.getElementById('startScript').addEventListener('click', () => {
this.control.start();
});
document.getElementById('stopScript').addEventListener('click', () => {
this.control.stop();
});
},
// 初始化抢课模块
init: function() {
// 只在课程选择页面初始化
if (!EducationHelper.Config.pageType.isCoursePage) return;
EducationHelper.Logger.action('初始化抢课模块...');
EducationHelper.Status.setNextAction('等待添加课程代码或开始识别');
EducationHelper.Status.syncFromState();
// 其他初始化操作
return this;
}
},
// 评估模块 - 处理单个评估页面
Evaluator: {
// 选项选择功能
optionSelector: {
getOptionCandidates: function() {
return Array.from(document.querySelectorAll(BrowserHelperCore.selectors.evaluationChoices))
.map(element => {
const parentText = EducationHelper.Utils.getElementText(
element.closest('label, td, tr, li, div')
);
const siblingText = EducationHelper.Utils.normalizeText(
[
element.nextElementSibling?.textContent || '',
element.parentElement?.textContent || '',
].join(' ')
);
const text = EducationHelper.Utils.normalizeText(`${parentText} ${siblingText}`);
return {
element,
text,
};
});
},
// 根据所选字母选择选项
selectByLetter: function(letter) {
EducationHelper.Logger.action(`正在选择${letter}选项...`);
// 更新配置中的选择
EducationHelper.Config.state.selectedOption = letter;
try {
const normalizedLetter = letter.toUpperCase();
const candidates = this.getOptionCandidates();
const matchedCandidates = candidates.filter(candidate =>
candidate.text.includes(`(${normalizedLetter})`) ||
candidate.text.includes(`${normalizedLetter} `)
);
let selectedCount = 0;
if (matchedCandidates.length > 0 && EducationHelper.Utils.handleDryRun(
matchedCandidates.map(candidate => candidate.element),
`已识别 ${matchedCandidates.length} 个 ${letter} 选项,演练模式下不会真正勾选`
)) {
EducationHelper.Status.setRecognized(`可选 ${letter} 项 ${matchedCandidates.length} 个`);
return true;
}
matchedCandidates.forEach(candidate => {
if (EducationHelper.Utils.clickElement(candidate.element)) {
selectedCount++;
}
});
if (selectedCount === 0 && typeof $ === 'function') {
$('.ace').each(function() {
const self = $(this);
const text = self.next().next().html();
if (text && text.indexOf(`(${normalizedLetter})`) !== -1) {
self.click();
selectedCount += 1;
}
});
}
if (selectedCount > 0) {
EducationHelper.Status.setRecognized(`已选择 ${selectedCount} 个${letter}选项`);
EducationHelper.Logger.success(`已选择 ${selectedCount} 个${letter}选项`);
return true;
}
EducationHelper.Logger.warn(`未找到可用的${letter}选项`);
EducationHelper.Status.setRecognized(`未找到 ${letter} 选项`);
return false;
} catch (error) {
EducationHelper.Logger.error(`选择${letter}选项时出错`, error);
return false;
}
}
},
// 评价内容填写
contentFiller: {
// 填写评价内容
fillContent: function(content) {
content = content || EducationHelper.Config.content.evaluationComment;
EducationHelper.Logger.action('正在填写评价内容...');
try {
// 尝试多种选择器找到文本区域
let filled = false;
// 1. 通过name属性查找
const mainTextarea = document.querySelector('textarea[name="zgpj"]');
if (mainTextarea) {
if (EducationHelper.Utils.handleDryRun(mainTextarea, '已识别主观评价文本框,演练模式下不会真正填写')) {
EducationHelper.Status.setRecognized('已识别主观评价文本框');
return true;
}
mainTextarea.value = content;
// 触发change事件
const event = new Event('input', { bubbles: true });
mainTextarea.dispatchEvent(event);
EducationHelper.Status.setRecognized('已填写主观评价文本框');
EducationHelper.Logger.success("已通过name='zgpj'找到并填写主观评价文本框");
filled = true;
}
// 2. 查找可见的文本区域
if (!filled) {
const textareas = Array.from(document.querySelectorAll('textarea.form-control, textarea'))
.filter(textarea => EducationHelper.Utils.isElementVisible(textarea));
if (textareas.length > 0) {
if (EducationHelper.Utils.handleDryRun(textareas, `已识别 ${textareas.length} 个文本框,演练模式下不会真正填写`)) {
EducationHelper.Status.setRecognized(`文本框 ${textareas.length} 个`);
return true;
}
for (const textarea of textareas) {
textarea.value = content;
const event = new Event('input', { bubbles: true });
textarea.dispatchEvent(event);
}
EducationHelper.Status.setRecognized(`已填写 ${textareas.length} 个文本框`);
EducationHelper.Logger.success("已填写所有文本框");
filled = true;
}
}
// 3. 兼容旧页面结构的 jQuery 兜底
if (!filled && typeof $ === 'function') {
const jqTextarea = $("#page-content-template > div > div > div.widget-content > form > div > table > tbody > tr:nth-child(25) > td > div > textarea");
if (jqTextarea.length > 0) {
jqTextarea.val(content);
EducationHelper.Status.setRecognized('已填写主观评价文本框');
EducationHelper.Logger.success("已使用jQuery选择器填写评价内容");
filled = true;
}
}
// 4. 最后尝试任何文本区域
if (!filled) {
const allTextareas = document.querySelectorAll('textarea');
if (allTextareas.length > 0) {
for (const textarea of allTextareas) {
textarea.value = content;
const event = new Event('input', { bubbles: true });
textarea.dispatchEvent(event);
}
EducationHelper.Status.setRecognized(`已填写 ${allTextareas.length} 个文本框`);
EducationHelper.Logger.success("已填写所有找到的文本区域");
filled = true;
}
}
return filled;
} catch (error) {
EducationHelper.Logger.error("填写评价内容时出错", error);
return false;
}
}
},
// 评价提交处理
submitter: {
// 倒计时组件
countdown: {
timer: null,
seconds: 0,
// 开始倒计时
start: function(duration, onComplete) {
// 停止现有倒计时
this.stop();
// 设置初始秒数
this.seconds = duration || EducationHelper.Config.timers.autoSubmitDelay / 1000;
// 确保倒计时显示元素存在且可见
this.ensureDisplayExists();
// 更新显示
this.updateDisplay();
// 开始倒计时
this.timer = EducationHelper.Runtime.setInterval(() => {
this.seconds--;
this.updateDisplay();
if (this.seconds <= 0) {
this.stop();
if (typeof onComplete === 'function') {
onComplete();
}
}
}, 1000);
return this;
},
// 停止倒计时
stop: function() {
if (this.timer) {
EducationHelper.Runtime.clearInterval(this.timer);
this.timer = null;
}
return this;
},
// 确保倒计时显示元素存在
ensureDisplayExists: function() {
// 检查是否已存在倒计时显示
let timerDisplay = document.getElementById('timerDisplay');
// 如果不存在,创建一个新的
if (!timerDisplay) {
EducationHelper.Logger.action('创建倒计时显示元素');
timerDisplay = document.createElement('div');
timerDisplay.id = 'timerDisplay';
timerDisplay.className = 'timer-display';
// 添加到UI容器中
const uiContent = document.getElementById('uiContent');
if (uiContent) {
// 添加到UI内容区的合适位置
const buttons = uiContent.querySelector('div[style*="display: flex"]');
if (buttons) {
uiContent.insertBefore(timerDisplay, buttons);
} else {
// 如果找不到按钮区域,添加到内容区最后
uiContent.appendChild(timerDisplay);
}
} else {
// 如果找不到UI容器,添加到body
document.body.appendChild(timerDisplay);
}
}
// 确保样式正确
timerDisplay.style.display = 'block';
timerDisplay.style.backgroundColor = '#e3f2fd';
timerDisplay.style.border = '1px solid #1976d2';
timerDisplay.style.padding = '10px';
timerDisplay.style.borderRadius = '4px';
timerDisplay.style.marginBottom = '10px';
timerDisplay.style.fontWeight = 'bold';
timerDisplay.style.fontSize = '16px';
timerDisplay.style.textAlign = 'center';
timerDisplay.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
// 设置初始内容
if (!timerDisplay.innerHTML || !timerDisplay.innerHTML.includes('timerValue')) {
timerDisplay.innerHTML = '等待提交: 120 秒';
}
// 如果是自动模式,使用不同的样式
if (EducationHelper.Config.state.autoMode) {
timerDisplay.style.backgroundColor = '#e8f5e9';
timerDisplay.style.border = '1px solid #4caf50';
timerDisplay.innerHTML = '自动提交倒计时: 120 秒';
}
return timerDisplay;
},
// 更新倒计时显示
updateDisplay: function() {
const timerElement = document.getElementById('timerValue');
if (timerElement) {
timerElement.textContent = this.seconds;
}
return this;
}
},
findSubmitButton: function() {
const candidates = Array.from(document.querySelectorAll(BrowserHelperCore.selectors.submitAction))
.filter(element => EducationHelper.Utils.isElementVisible(element))
.map(element => {
const text = EducationHelper.Utils.getElementText(element);
const rect = element.getBoundingClientRect();
return {
element,
score: BrowserHelperCore.scoreSubmitCandidate({
text,
type: element.type,
className: element.className,
rectTop: rect.top,
viewportHeight: window.innerHeight,
})
};
})
.filter(item => item.score > 0)
.sort((a, b) => b.score - a.score);
return candidates[0]?.element || null;
},
findConfirmButton: function() {
const exactMatch = EducationHelper.Utils.findClickableByText(
['确定', '确认', '是'],
BrowserHelperCore.selectors.confirmAction
);
return exactMatch;
},
// 查找并点击提交按钮
findAndClickSubmitButton: async function(showMessage = true) {
EducationHelper.Logger.action('查找提交按钮...');
// 显示状态消息
let statusMsg = null;
if (showMessage) {
statusMsg = EducationHelper.UI.showMessage('正在提交评价...
', 'info', 0); // 0表示不自动消失
}
const submitButton = await EducationHelper.Utils.waitFor(
() => this.findSubmitButton() || document.querySelector('#submit, #btnSubmit, .submit-btn, [name="submit"]'),
{ timeout: 4000, message: '未找到提交按钮' }
).catch(() => null);
if (submitButton) {
if (statusMsg) {
statusMsg.innerHTML = '找到提交按钮,正在点击...
';
}
if (EducationHelper.Utils.handleDryRun(submitButton, '已识别提交按钮,演练模式下不会真正提交评价')) {
if (statusMsg && document.contains(statusMsg)) {
statusMsg.remove();
}
return true;
}
EducationHelper.Logger.action("找到提交按钮,点击提交");
EducationHelper.Utils.clickElement(submitButton);
await this.handleConfirmDialog(statusMsg);
return true;
}
EducationHelper.Logger.warn("未找到提交按钮");
if (statusMsg) {
statusMsg.innerHTML = '未找到提交按钮,尝试直接提交表单...
';
}
const mainForm = document.querySelector('form');
if (mainForm) {
EducationHelper.Logger.action("尝试直接提交表单");
try {
mainForm.submit();
EducationHelper.Logger.success("已调用表单的submit()方法");
if (statusMsg) {
statusMsg.innerHTML = '已尝试提交表单,请检查是否成功
';
EducationHelper.Runtime.setTimeout(() => {
if (statusMsg && document.contains(statusMsg)) {
statusMsg.remove();
}
}, 2000);
}
return true;
} catch (error) {
EducationHelper.Logger.error("尝试提交表单失败", error);
}
}
if (statusMsg) {
statusMsg.innerHTML = '自动提交失败,请手动点击页面中的"提交"按钮
';
EducationHelper.Runtime.setTimeout(() => {
if (statusMsg && document.contains(statusMsg)) {
statusMsg.remove();
}
}, 5000);
}
return false;
},
// 处理确认对话框
handleConfirmDialog: async function(statusMsg) {
EducationHelper.Logger.action("等待确认对话框...");
const confirmButton = await EducationHelper.Utils.waitFor(
() => this.findConfirmButton(),
{ timeout: 3000, message: '未出现确认按钮' }
).catch(() => null);
if (!confirmButton) {
EducationHelper.Logger.action("未找到确认按钮,可能评价已直接提交或需要手动确认");
if (statusMsg) {
statusMsg.innerHTML = '可能需要手动确认提交,请检查是否有弹出确认窗口
';
EducationHelper.Runtime.setTimeout(() => {
if (statusMsg && document.contains(statusMsg)) {
statusMsg.remove();
}
}, 5000);
}
return false;
}
EducationHelper.Logger.action("找到确认按钮,确认提交");
if (statusMsg) {
statusMsg.innerHTML = '找到确认按钮,正在确认提交...
';
}
EducationHelper.Utils.clickElement(confirmButton);
if (statusMsg) {
statusMsg.innerHTML = '评价提交成功!
';
EducationHelper.Runtime.setTimeout(() => {
if (statusMsg && document.contains(statusMsg)) {
statusMsg.remove();
}
}, 2000);
}
EducationHelper.Logger.success("已完成评价提交");
EducationHelper.UI.showMessage(`
评价提交成功
可以继续处理下一个评估项。
`, 'success', 2000);
if (EducationHelper.Config.state.autoMode) {
EducationHelper.Runtime.setTimeout(() => {
const fallbackCandidates = Array.from(document.querySelectorAll(BrowserHelperCore.selectors.backAction))
.filter(element => EducationHelper.Utils.isElementVisible(element))
.map(element => ({
element,
text: EducationHelper.Utils.getElementText(element),
href: element.getAttribute('href') || '',
}));
const bestBackCandidate = BrowserHelperCore.pickBestBackCandidate(fallbackCandidates);
const backBtn = EducationHelper.Utils.findClickableByText(['返回', '列表', '评估列表']) ||
bestBackCandidate?.element;
if (backBtn) {
EducationHelper.Logger.action("找到返回按钮,自动返回列表页");
EducationHelper.Utils.clickElement(backBtn);
} else {
EducationHelper.Logger.warn('未找到返回列表入口,尝试 history.back()');
window.history.back();
}
}, 1500);
}
return true;
}
},
// 流程控制
process: {
// 启动自动评价流程
start: async function() {
EducationHelper.Logger.action('开始评价流程...');
EducationHelper.Status.setNextAction('识别题项、填写评价内容');
await EducationHelper.Utils.waitFor(
() => document.querySelector(BrowserHelperCore.selectors.evaluationInputs),
{ timeout: 10000, message: '评价页面元素未加载完成' }
).catch(error => {
EducationHelper.Logger.warn(error.message);
return null;
});
// 显示状态消息
EducationHelper.UI.showMessage(
'正在进行评价操作...
',
'info',
2000
);
// 1. 选择选项
EducationHelper.Evaluator.optionSelector.selectByLetter(
EducationHelper.Config.state.selectedOption
);
if (EducationHelper.Config.state.dryRunMode) {
EducationHelper.Evaluator.contentFiller.fillContent(
document.getElementById('evaluationContent')?.value ||
EducationHelper.Config.content.evaluationComment
);
EducationHelper.Status.setNextAction('关闭演练模式后可执行真实评价流程');
return;
}
// 2. 填写评价内容
EducationHelper.Runtime.setTimeout(() => {
EducationHelper.Evaluator.contentFiller.fillContent(
document.getElementById('evaluationContent')?.value ||
EducationHelper.Config.content.evaluationComment
);
// 禁用开始按钮,防止重复点击
const startButton = document.getElementById('startEvaluation');
if (startButton) {
startButton.disabled = true;
startButton.style.opacity = '0.6';
startButton.textContent = '评价已开始';
}
// 3. 如果自动提交已启用,启动倒计时
if (EducationHelper.Config.state.autoSubmitEnabled) {
EducationHelper.Logger.action('已启用自动提交,开始倒计时...');
EducationHelper.Status.setNextAction('等待 120 秒后自动提交');
// 启动倒计时
EducationHelper.Evaluator.submitter.countdown.start(
EducationHelper.Config.timers.autoSubmitDelay / 1000,
// 倒计时结束回调
() => {
if (EducationHelper.Config.state.autoSubmitEnabled) {
EducationHelper.Logger.action('倒计时结束,自动提交评价');
// 提示用户即将提交
EducationHelper.UI.showMessage(
`倒计时结束
正在自动提交评价...
`,
'info',
2000
);
// 提交评价
EducationHelper.Runtime.setTimeout(() => {
EducationHelper.Evaluator.process.submit();
}, 2000);
}
}
);
}
}, 500);
},
// 提交评价
submit: async function() {
EducationHelper.Logger.action('提交评价...');
EducationHelper.Status.setNextAction('识别提交按钮并尝试提交');
// 保存评价内容到配置
const contentElement = document.getElementById('evaluationContent');
if (contentElement) {
EducationHelper.Config.content.evaluationComment = contentElement.value;
// 保存到localStorage
EducationHelper.Config.saveSettings();
}
// 再次确认文本框已填写
EducationHelper.Evaluator.contentFiller.fillContent();
// 查找并点击提交按钮
await EducationHelper.Evaluator.submitter.findAndClickSubmitButton();
}
},
// UI生成
generateUI: function() {
return `
${EducationHelper.Status.generatePanel()}
等待说明
教务系统要求等待 120 秒,脚本不会跳过,只会自动填写并到点提交。
等待提交: 120 秒
`;
},
// 事件绑定
bindEvents: function() {
document.getElementById('evaluationDryRunMode').addEventListener('change', function() {
EducationHelper.Config.state.dryRunMode = this.checked;
EducationHelper.Config.saveSettings();
EducationHelper.Status.syncFromState();
EducationHelper.Logger.action(`评估演练模式: ${this.checked ? '已启用' : '已关闭'}`);
});
// 选项单选按钮
const optionRadios = Array.from(document.getElementsByName('evaluationOption'));
optionRadios.forEach(radio => {
radio.addEventListener('change', function() {
EducationHelper.Config.state.selectedOption = this.value;
EducationHelper.Logger.action(`已选择${this.value}选项`);
EducationHelper.Config.saveSettings();
});
});
// 自动提交复选框
document.getElementById('autoSubmitEvaluation').addEventListener('change', function() {
EducationHelper.Config.state.autoSubmitEnabled = this.checked;
EducationHelper.Logger.action(`自动提交评价: ${this.checked ? '已启用' : '已禁用'}`);
EducationHelper.Status.setNextAction(this.checked ? '等待 120 秒后自动提交' : '等待用户手动提交');
EducationHelper.Config.saveSettings();
});
// 评价内容输入框
document.getElementById('evaluationContent').addEventListener('input', function() {
EducationHelper.Config.content.evaluationComment = this.value;
EducationHelper.Status.setRecognized(`题项 ${document.querySelectorAll(BrowserHelperCore.selectors.evaluationChoices).length} / 文本框 ${document.querySelectorAll('textarea').length}`);
});
// 选择选项按钮
document.getElementById('selectOptions').addEventListener('click', () => {
EducationHelper.Evaluator.optionSelector.selectByLetter(
EducationHelper.Config.state.selectedOption
);
// 如果启用了自动提交,则应用选项后自动提交
if (EducationHelper.Config.state.autoSubmitEnabled) {
EducationHelper.Logger.action('已启用自动提交,将在3秒后提交评价...');
EducationHelper.Status.setNextAction(EducationHelper.Config.state.dryRunMode ? '演练模式下仅预览提交按钮' : '即将尝试提交评价');
EducationHelper.Runtime.setTimeout(() => {
EducationHelper.Evaluator.process.submit();
}, 3000);
}
});
// 立即提交评价按钮
document.getElementById('submitEvaluation').addEventListener('click', () => {
EducationHelper.Evaluator.process.submit();
});
// 开始评价按钮
document.getElementById('startEvaluation').addEventListener('click', () => {
EducationHelper.Evaluator.process.start();
});
},
// 初始化
init: function() {
if (!EducationHelper.Config.pageType.isEvaluationPage) return;
EducationHelper.Logger.action('初始化评估模块...');
EducationHelper.Status.setNextAction('等待开始评价流程');
EducationHelper.Status.syncFromState();
// 检查localStorage中是否存在autoMode标记
if (EducationHelper.Config.state.autoMode) {
EducationHelper.Logger.action('检测到全自动模式,自动开始评价流程');
EducationHelper.Runtime.setTimeout(() => {
// 自动执行评价流程
this.process.start();
}, 1000);
}
return this;
}
},
// 评估列表模块 - 处理评估列表页面
EvaluationList: {
itemHelpers: {
getActionElements: function() {
const root = EducationHelper.Utils.getEvaluationSearchRoot();
return Array.from(root.querySelectorAll(BrowserHelperCore.selectors.evaluationAction))
.filter(element => EducationHelper.Utils.isElementVisible(element))
.filter(element => !EducationHelper.Utils.isIgnoredEvaluationElement(element));
},
isEvaluationAction: function(element) {
const text = EducationHelper.Utils.getElementText(element);
return !EducationHelper.Utils.isIgnoredEvaluationElement(element) &&
BrowserHelperCore.isEvaluationActionText(text);
},
getContext: function(element) {
const context = element.closest(BrowserHelperCore.selectors.evaluationContainer) || element;
return EducationHelper.Utils.isIgnoredEvaluationElement(context) ? null : context;
},
isCompletedContext: function(context) {
const text = EducationHelper.Utils.getElementText(context);
return BrowserHelperCore.isCompletedText(text);
},
getEvaluationEntries: function() {
const root = EducationHelper.Utils.getEvaluationSearchRoot();
if (EducationHelper.Utils.hasEvaluationClosedNotice(root)) {
return [];
}
const entries = new Map();
this.getActionElements()
.filter(element => this.isEvaluationAction(element))
.forEach(element => {
const context = this.getContext(element);
if (!context) {
return;
}
if (!entries.has(context)) {
entries.set(context, {
context,
actions: [],
});
}
entries.get(context).actions.push(element);
});
const normalizedEntries = Array.from(entries.values()).map(entry => {
const enabledActions = entry.actions.filter(action =>
!EducationHelper.Utils.isElementDisabled(action)
);
return {
context: entry.context,
actions: entry.actions,
enabledActions,
isCompleted: this.isCompletedContext(entry.context) || enabledActions.length === 0,
};
});
if (normalizedEntries.length > 0) {
return normalizedEntries;
}
return Array.from(root.querySelectorAll('tr, .list-item, .evaluation-item'))
.filter(context => EducationHelper.Utils.isElementVisible(context))
.filter(context => !EducationHelper.Utils.isIgnoredEvaluationElement(context))
.map(context => {
const text = EducationHelper.Utils.getElementText(context);
if (!text || (!text.includes('评估') && !text.includes('评价') && !text.includes('教师'))) {
return null;
}
return {
context,
actions: [],
enabledActions: [],
isCompleted: this.isCompletedContext(context),
};
})
.filter(Boolean);
},
getPendingActions: function() {
return this.getEvaluationEntries()
.filter(entry => !entry.isCompleted)
.flatMap(entry => entry.enabledActions)
.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
}
},
// 进度统计
progress: {
total: 0,
completed: 0,
observer: null,
updateTimer: null,
scheduleUpdate: function() {
EducationHelper.Runtime.clearTimeout(this.updateTimer);
this.updateTimer = EducationHelper.Runtime.setTimeout(() => {
this.updateProgress();
}, 150);
},
startTracking: function() {
this.stopTracking();
this.observer = EducationHelper.Runtime.observe(
document.body || document.documentElement,
{ childList: true, subtree: true, attributes: true },
() => this.scheduleUpdate()
);
},
stopTracking: function() {
EducationHelper.Runtime.disconnectObserver(this.observer);
this.observer = null;
EducationHelper.Runtime.clearTimeout(this.updateTimer);
this.updateTimer = null;
},
// 统计评价进度
updateProgress: function() {
try {
const entries = EducationHelper.EvaluationList.itemHelpers.getEvaluationEntries();
const totalCount = entries.length;
const completedCount = entries.filter(entry => entry.isCompleted).length;
this.total = totalCount;
this.completed = completedCount;
EducationHelper.Logger.debug(`进度统计: ${completedCount}/${totalCount}`);
// 更新UI显示
this.updateUI();
return { total: totalCount, completed: completedCount };
} catch (error) {
EducationHelper.Logger.error('统计评价进度时出错', error);
return { total: 0, completed: 0 };
}
},
// 更新进度显示UI
updateUI: function() {
const progressDiv = document.getElementById('evaluationProgress');
if (!progressDiv) return;
const percentage = this.total > 0 ? Math.round((this.completed / this.total) * 100) : 0;
const isCompleted = this.completed === this.total && this.total > 0;
const summaryText = this.total === 0
? '当前页面没有待评项目'
: (isCompleted ? '所有评价已完成' : `完成度 ${percentage}% · 还剩 ${this.total - this.completed} 个`);
progressDiv.innerHTML = `
评估进度
${this.completed}/${this.total}
${summaryText}
`;
}
},
// 自动点击功能
autoClicker: {
timerId: null,
scanDelayTimerId: null,
stopCountdown: function() {
EducationHelper.Runtime.clearInterval(this.timerId);
EducationHelper.Runtime.clearTimeout(this.scanDelayTimerId);
this.timerId = null;
this.scanDelayTimerId = null;
},
// 开始自动点击倒计时
startCountdown: function() {
EducationHelper.Logger.action(`开始自动点击倒计时,${EducationHelper.Config.timers.autoClickEvaluationDelay/1000}秒后开始执行...`);
this.stopCountdown();
// 设置初始倒计时值
const countdownElement = document.getElementById('countdownValue');
if (!countdownElement) return false;
let secondsLeft = EducationHelper.Config.timers.autoClickEvaluationDelay / 1000;
countdownElement.textContent = secondsLeft;
EducationHelper.Config.state.autoClickStopped = false;
EducationHelper.Status.setNextAction(
EducationHelper.Config.state.dryRunMode ?
'演练模式下将预览自动评估入口' :
'等待倒计时后自动点击评估入口'
);
// 创建新的倒计时
this.timerId = EducationHelper.Runtime.setInterval(() => {
// 每次检查是否已停止
if (EducationHelper.Config.state.autoClickStopped) {
this.stopCountdown();
EducationHelper.Logger.action('倒计时已被手动停止');
return;
}
secondsLeft -= 1;
countdownElement.textContent = secondsLeft;
// 倒计时结束,执行自动点击
if (secondsLeft <= 0) {
this.stopCountdown();
// 再次检查是否仍然启用了自动点击和未被停止
if ((EducationHelper.Config.state.autoClickEvaluationEnabled || EducationHelper.Config.state.autoMode) && !EducationHelper.Config.state.autoClickStopped) {
EducationHelper.Logger.action('倒计时结束,开始扫描评估按钮...');
// 更新倒计时文本
const countdownDiv = document.getElementById('countdownDiv');
if (countdownDiv) {
countdownDiv.innerHTML = '自动操作开始执行...';
}
// 延迟执行扫描,让用户有机会看到状态更新
this.scanDelayTimerId = EducationHelper.Runtime.setTimeout(() => {
// 再次检查,确保在延迟期间没有被停止
if (!EducationHelper.Config.state.autoClickStopped) {
// 扫描并点击评估按钮
EducationHelper.EvaluationList.scanner.scanAndClick();
// 扫描完成后隐藏倒计时区域
EducationHelper.Runtime.setTimeout(() => {
if (countdownDiv) {
countdownDiv.style.display = 'none';
}
// 自动模式下不关闭开关,保持自动状态
if (!EducationHelper.Config.state.autoMode) {
EducationHelper.Config.state.autoClickEvaluationEnabled = false;
const checkbox = document.getElementById('autoClickEvaluation');
if (checkbox) checkbox.checked = false;
}
}, 2000);
} else {
EducationHelper.Logger.action('在延迟期间检测到停止请求,取消自动操作');
if (countdownDiv) {
countdownDiv.style.display = 'none';
}
}
}, 500);
} else {
EducationHelper.Logger.action('倒计时结束,但自动点击已被禁用');
const countdownDiv = document.getElementById('countdownDiv');
if (countdownDiv) {
countdownDiv.style.display = 'none';
}
}
}
}, 1000);
return true;
}
},
// 扫描与点击
scanner: {
countdownTimerId: null,
delayedClickTimerId: null,
stopPendingClick: function() {
EducationHelper.Runtime.clearInterval(this.countdownTimerId);
EducationHelper.Runtime.clearTimeout(this.delayedClickTimerId);
this.countdownTimerId = null;
this.delayedClickTimerId = null;
},
// 扫描评估按钮并点击(仅自动模式)
scanAndClick: function() {
EducationHelper.Logger.action('自动扫描评估按钮开始...');
this.stopPendingClick();
EducationHelper.Preview.clear();
// 更新进度统计
EducationHelper.EvaluationList.progress.updateProgress();
const evaluationButtons = EducationHelper.EvaluationList.itemHelpers.getPendingActions();
EducationHelper.Status.setRecognized(`待评入口 ${evaluationButtons.length} 个`);
EducationHelper.Logger.action(`找到 ${evaluationButtons.length} 个评估按钮`);
// 自动模式处理
if (evaluationButtons.length > 0) {
if (EducationHelper.Config.state.dryRunMode) {
EducationHelper.Status.setNextAction('演练模式下仅预览待评入口');
EducationHelper.Utils.handleDryRun(
evaluationButtons,
`已识别 ${evaluationButtons.length} 个待评入口,演练模式下不会点击`
);
EducationHelper.UI.showMessage(
`演练模式
已识别 ${evaluationButtons.length} 个待评入口,仅预览不点击
`,
'info',
2500
);
return;
}
// 优先点击带有"评估"文本的按钮
const buttonToClick = evaluationButtons[0];
// 创建简洁的倒计时提示
let countdownSeconds = 3; // 缩短倒计时时间
// 显示简洁的状态提示
const statusMsg = EducationHelper.UI.showMessage(`
已找到待评入口
将在 ${countdownSeconds} 秒后点击
`, 'info', 0);
// 开始倒计时
this.countdownTimerId = EducationHelper.Runtime.setInterval(() => {
countdownSeconds--;
// 检查是否已手动停止
if (EducationHelper.Config.state.autoClickStopped) {
this.stopPendingClick();
if (statusMsg && document.contains(statusMsg)) {
statusMsg.remove();
}
return;
}
const countdownElement = document.getElementById('clickCountdown');
if (countdownElement) {
countdownElement.textContent = countdownSeconds;
}
if (countdownSeconds <= 0) {
EducationHelper.Runtime.clearInterval(this.countdownTimerId);
this.countdownTimerId = null;
// 更新提示信息
if (statusMsg && document.contains(statusMsg)) {
statusMsg.innerHTML = '正在点击评估按钮...
';
}
// 点击评估按钮
this.delayedClickTimerId = EducationHelper.Runtime.setTimeout(() => {
if (!EducationHelper.Config.state.autoClickStopped) {
this.clickButton(buttonToClick);
// 移除状态提示
if (statusMsg && document.contains(statusMsg)) {
statusMsg.remove();
}
}
}, 500);
}
}, 1000);
} else {
EducationHelper.UI.showMessage('未找到评估按钮', 'warning', 3000);
}
},
// 安全点击按钮
clickButton: function(button) {
EducationHelper.Logger.action(`点击按钮: ${button.outerHTML}`);
if (EducationHelper.Utils.clickElement(button)) {
EducationHelper.Status.setNextAction('已点击待评入口,等待进入评估页面');
EducationHelper.Logger.success('已通过事件触发点击按钮');
return true;
}
EducationHelper.Logger.error('无法点击按钮');
return false;
}
},
// 自动模式控制
autoMode: {
// 启用自动模式
enable: function() {
EducationHelper.Config.state.autoMode = true;
EducationHelper.Config.state.autoClickEvaluationEnabled = true;
EducationHelper.Config.state.autoClickStopped = false;
EducationHelper.Logger.action('全自动模式已启用');
const checkbox = document.getElementById('autoClickEvaluation');
if (checkbox) checkbox.checked = true;
EducationHelper.Config.saveSettings();
// 显示倒计时并开始自动点击
const countdownDiv = document.getElementById('countdownDiv');
if (countdownDiv) {
countdownDiv.style.display = 'block';
countdownDiv.innerHTML = `
自动流程已启动
将在 ${EducationHelper.Config.timers.autoClickEvaluationDelay/1000} 秒后开始执行
`;
}
// 开始倒计时
EducationHelper.EvaluationList.autoClicker.startCountdown();
// 显示提示消息
EducationHelper.UI.showMessage(`
自动流程已启动
脚本会自动进入待评项、填写内容并提交。
需要时可随时停止。
`, 'success', 3500);
return true;
},
// 禁用自动模式
disable: function() {
EducationHelper.Config.state.autoMode = false;
EducationHelper.Status.setNextAction('等待手动开始');
EducationHelper.Logger.action('全自动模式已禁用');
EducationHelper.shutdown('停止全自动模式', { keepProgressTracking: true });
EducationHelper.UI.showMessage(`
自动流程已停止
后续操作将保持手动模式。
`, 'error', 2600);
return true;
}
},
// UI生成
generateUI: function() {
return `
${EducationHelper.Status.generatePanel()}
自动操作倒计时
${EducationHelper.Config.timers.autoClickEvaluationDelay/1000} 秒
操作提示
自动点击默认关闭。全自动模式会依次进入待评项、填写内容并提交,操作过程中可随时取消。
更多设置
`;
},
// 事件绑定
bindEvents: function() {
// 自动点击评估按钮复选框
document.getElementById('evaluationListDryRunMode').addEventListener('change', function() {
EducationHelper.Config.state.dryRunMode = this.checked;
EducationHelper.Config.saveSettings();
EducationHelper.Status.syncFromState();
EducationHelper.Logger.action(`评估列表演练模式: ${this.checked ? '已启用' : '已关闭'}`);
});
document.getElementById('autoClickEvaluation').addEventListener('change', function() {
EducationHelper.Config.state.autoClickEvaluationEnabled = this.checked;
EducationHelper.Config.state.autoClickStopped = !this.checked;
EducationHelper.Status.setNextAction(
this.checked ?
(EducationHelper.Config.state.dryRunMode ? '等待倒计时后预览评估入口' : '等待倒计时后点击评估入口') :
'等待手动开始'
);
EducationHelper.Logger.action(`自动点击评估按钮: ${this.checked ? '已启用' : '已禁用'}`);
// 显示或隐藏倒计时区域
const countdownDiv = document.getElementById('countdownDiv');
if (this.checked) {
countdownDiv.style.display = 'block';
EducationHelper.EvaluationList.autoClicker.startCountdown();
} else {
EducationHelper.Status.setNextAction('暂未识别到待评项目');
countdownDiv.style.display = 'none';
EducationHelper.EvaluationList.autoClicker.stopCountdown();
EducationHelper.EvaluationList.scanner.stopPendingClick();
EducationHelper.Status.setNextAction('等待手动开始');
}
EducationHelper.Config.saveSettings();
EducationHelper.Status.syncFromState();
});
// 全自动模式按钮
document.getElementById('autoModeButton').addEventListener('click', function() {
// 根据当前状态切换自动模式
if (EducationHelper.Config.state.autoMode) {
EducationHelper.EvaluationList.autoMode.disable();
} else {
EducationHelper.EvaluationList.autoMode.enable();
}
// 更新按钮样式
this.style.background = EducationHelper.Config.state.autoMode ? '#DC2626' : '#2563EB';
this.textContent = EducationHelper.Config.state.autoMode ? '停止自动流程' : '启动自动流程';
EducationHelper.Status.syncFromState();
});
// 调试模式复选框
document.getElementById('debugMode').addEventListener('change', function() {
EducationHelper.Config.state.debugMode = this.checked;
EducationHelper.Logger.action(`调试模式: ${this.checked ? '已启用' : '已禁用'}`);
EducationHelper.Config.saveSettings();
// 显示/隐藏调试面板
const debugPanel = document.getElementById('debugInfo');
if (debugPanel) {
debugPanel.style.display = this.checked ? 'block' : 'none';
}
// 如果启用调试模式,输出页面基本信息
if (this.checked) {
EducationHelper.Logger.debug('调试模式已启用');
EducationHelper.Logger.debug(`当前URL: ${window.location.href}`);
EducationHelper.Logger.debug(`页面标题: ${document.title}`);
EducationHelper.Logger.debug(`jQuery可用: ${typeof $ !== 'undefined'}`);
}
});
},
// 初始化
init: function() {
if (!EducationHelper.Config.pageType.isEvaluationListPage) return;
EducationHelper.Status.setNextAction('等待扫描评估条目');
EducationHelper.Status.syncFromState();
EducationHelper.Logger.action('初始化评估列表模块...');
// 初始化进度统计
this.progress.startTracking();
EducationHelper.Runtime.setTimeout(() => {
this.progress.updateProgress();
}, 200);
// 如果启用了自动点击,则设置倒计时
if (EducationHelper.Config.state.autoClickEvaluationEnabled || EducationHelper.Config.state.autoMode) {
// 等待UI元素创建完成后启动倒计时
EducationHelper.Runtime.setTimeout(() => {
const countdownDiv = document.getElementById('countdownDiv');
if (countdownDiv) {
countdownDiv.style.display = 'block';
// 开始倒计时
this.autoClicker.startCountdown();
}
}, 500);
}
return this;
}
},
};
// 初始化配置
EducationHelper.Config.init();
// 主入口点 - 脚本初始化
function init() {
console.log('齐大教务助手启动中...');
// 等待DOM加载完成
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initAfterDOMLoaded);
} else {
initAfterDOMLoaded();
}
}
// DOM加载完成后初始化
function initAfterDOMLoaded() {
// 创建UI界面
EducationHelper.UI.create();
// 根据页面类型初始化相应模块
initModulesByPageType();
}
// 根据页面类型初始化模块
function initModulesByPageType() {
const config = EducationHelper.Config;
if (config.pageType.isCoursePage) {
// 生成UI内容
EducationHelper.UI.elements.content.innerHTML = EducationHelper.CourseGrabber.generateUI();
EducationHelper.UI.syncViewportConstraints();
// 绑定事件
EducationHelper.CourseGrabber.bindEvents();
// 初始化抢课模块
EducationHelper.CourseGrabber.init();
}
else if (config.pageType.isEvaluationPage) {
EducationHelper.UI.elements.content.innerHTML = EducationHelper.Evaluator.generateUI();
EducationHelper.UI.syncViewportConstraints();
EducationHelper.Evaluator.bindEvents();
// 评估页面初始化代码
EducationHelper.Evaluator.init();
}
else if (config.pageType.isEvaluationListPage) {
EducationHelper.UI.elements.content.innerHTML = EducationHelper.EvaluationList.generateUI();
EducationHelper.UI.syncViewportConstraints();
EducationHelper.EvaluationList.bindEvents();
// 评估列表页面初始化代码
EducationHelper.EvaluationList.init();
}
else {
// 不支持的页面
EducationHelper.UI.elements.content.innerHTML = `
`;
EducationHelper.UI.syncViewportConstraints();
}
}
// 启动脚本
init();
})();