// ==UserScript==
// @name 南理工教务增强助手 v1.5
// @namespace http://tampermonkey.net/
// @version 1.5
// @description 在合适的地方显示课程大纲、选修课类别及选修课学分情况,并自动刷新登录(不可用)状态。
// @match 202.119.81.112/*
// @match bkjw.njust.edu.cn/*
// @match 202.119.81.112:9080/*
// @match 202.119.81.113:9080/*
// @grant GM_xmlhttpRequest
// @connect jsdelivr.net
// @connect njust.wiki
// @author Light
// @license MIT
// @supportURL https://github.com/NJUST-OpenLib/NJUST-JWC-Enhance
// ==/UserScript==
// ==================== 远程数据源配置 ====================
// 选修课分类数据源
const CATEGORY_URL = 'https://fastly.jsdelivr.net/npm/njust-jwc-enhance@latest/data/xxk.json';
// 课程大纲数据源
const OUTLINE_URL = 'https://fastly.jsdelivr.net/npm/njust-jwc-enhance@latest/data/kcdg.json';
// 备用数据源(如需要可取消注释)Q
// const CATEGORY_URL = 'https://fastly.jsdelivr.net/gh/NJUST-OpenLib/NJUST-JWC-Enhance@latest/data/xxk.json';
// const OUTLINE_URL = 'https://fastly.jsdelivr.net/gh/NJUST-OpenLib/NJUST-JWC-Enhance@latest/data/kcdg.json';
(function () {
'use strict';
// ==================== 配置选项 ====================
// 用户界面配置
const UI_CONFIG = {
showNotifications: true // 是否显示前端提示框 (true=显示,false=隐藏)
// 设置为 false 可完全关闭所有状态提示框
// 设置为 true 则正常显示加载、成功、错误等提示
};
// 调试配置
const DEBUG_CONFIG = {
enabled: false, // 是否启用调试
level: 0, // 调试级别: 0=关闭,1=错误,2=警告,3=信息,4=详细
showCache: true // 是否显示缓存相关日志
};
// 缓存配置
const CACHE_CONFIG = {
enabled: true, // 是否启用缓存
ttl: 600, // 缓存生存时间 (秒)
prefix: 'njust_jwc_enhance_' // 缓存键前缀
};
// ==================== 调试系统 ====================
const Logger = {
LEVELS: { ERROR: 1, WARN: 2, INFO: 3, DEBUG: 4 },
log(level, message, ...args) {
if (!DEBUG_CONFIG.enabled || level > DEBUG_CONFIG.level) return;
const timestamp = new Date().toLocaleTimeString();
const levelNames = ['', '❌', '⚠️', 'ℹ️', '🔍'];
const prefix = `[${timestamp}] ${levelNames[level]} [南理工教务助手]`;
console.log(prefix, message, ...args);
// 对于 INFO 级别的消息,同时通过状态提示框显示(如果启用)
if (level === this.LEVELS.INFO && UI_CONFIG.showNotifications && typeof StatusNotifier !== 'undefined' && StatusNotifier.show) {
try {
// 提取纯文本消息,去除表情符号前缀
let cleanMessage = message.replace(/^[🎯🚀📊🎓🚪💾✅🗑️⏰❌🔍⚠️ℹ️]+\s*/, '');
// 如果有额外参数,将其格式化并添加到消息中
if (args.length > 0) {
const formattedArgs = args.map(arg => {
if (typeof arg === 'object' && arg !== null) {
try {
// 安全的对象序列化,避免循环引用
const seen = new WeakSet();
const jsonStr = JSON.stringify(arg, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular Reference]';
}
seen.add(value);
}
return value;
}, 0);
// 如果 JSON 字符串太长,进行适当格式化
if (jsonStr.length > 200) {
// 对于长对象,使用更紧凑的格式,限制深度
return Object.entries(arg)
.slice(0, 10) // 限制显示前 10 个属性
.map(([key, value]) => {
let valueStr;
if (typeof value === 'object' && value !== null) {
valueStr = '[Object]';
} else {
valueStr = String(value).slice(0, 50); // 限制值长度
}
return `${key}: ${valueStr}`;
})
.join(', ') + (Object.keys(arg).length > 10 ? '...' : '');
} else {
// 移除 JSON 的花括号,使其更易读
return jsonStr.replace(/^{|}$/g, '').replace(/"/g, '');
}
} catch (e) {
// 如果 JSON.stringify 失败,使用安全的回退方法
try {
return Object.entries(arg)
.slice(0, 5) // 限制属性数量
.map(([key, value]) => `${key}: ${String(value).slice(0, 30)}`)
.join(', ') + (Object.keys(arg).length > 5 ? '...' : '');
} catch (e2) {
return '[Object - Cannot Display]';
}
}
}
return String(arg).slice(0, 100); // 限制字符串长度
}).join(' ');
cleanMessage += ' ' + formattedArgs;
}
StatusNotifier.show(cleanMessage, 'info');
} catch (e) {
// 静默处理状态提示框错误,避免影响日志功能
}
}
},
error(message, ...args) { this.log(this.LEVELS.ERROR, message, ...args); },
warn(message, ...args) { this.log(this.LEVELS.WARN, message, ...args); },
info(message, ...args) { this.log(this.LEVELS.INFO, message, ...args); },
debug(message, ...args) { this.log(this.LEVELS.DEBUG, message, ...args); }
};
// ==================== 缓存系统 ====================
const CacheManager = {
// 获取缓存键
getKey(url) {
return CACHE_CONFIG.prefix + btoa(url).replace(/[^a-zA-Z0-9]/g, '');
},
// 设置缓存
set(url, data) {
if (!CACHE_CONFIG.enabled) return false;
try {
const cacheData = {
data: data,
timestamp: Date.now(),
ttl: CACHE_CONFIG.ttl * 1000,
url: url
};
const key = this.getKey(url);
localStorage.setItem(key, JSON.stringify(cacheData));
if (DEBUG_CONFIG.showCache) {
Logger.info(`💾 缓存已保存: ${url}`, {
key: key,
size: JSON.stringify(cacheData).length + ' bytes',
ttl: CACHE_CONFIG.ttl + 's'
});
}
return true;
} catch (e) {
Logger.error('缓存保存失败: ', e);
return false;
}
},
// 获取缓存
get(url) {
if (!CACHE_CONFIG.enabled) return null;
try {
const key = this.getKey(url);
const cached = localStorage.getItem(key);
if (!cached) {
if (DEBUG_CONFIG.showCache) {
Logger.debug(`❌ 缓存未命中: ${url}`);
}
return null;
}
const cacheData = JSON.parse(cached);
const now = Date.now();
const age = (now - cacheData.timestamp) / 1000;
const remaining = (cacheData.ttl - (now - cacheData.timestamp)) / 1000;
// 检查是否过期
if (now - cacheData.timestamp > cacheData.ttl) {
localStorage.removeItem(key);
if (DEBUG_CONFIG.showCache) {
Logger.warn(`⏰ 缓存已过期: ${url}`, {
age: age.toFixed(1) + 's',
expired: (age - CACHE_CONFIG.ttl).toFixed(1) + 's ago'
});
}
return null;
}
if (DEBUG_CONFIG.showCache) {
Logger.info(`✅ 缓存命中: ${url}`, {
age: age.toFixed(1) + 's',
remaining: remaining.toFixed(1) + 's',
size: cached.length + ' bytes'
});
}
return cacheData.data;
} catch (e) {
Logger.error('缓存读取失败: ', e);
return null;
}
},
// 清除所有缓存
clear() {
try {
const keys = Object.keys(localStorage).filter(key =>
key.startsWith(CACHE_CONFIG.prefix)
);
keys.forEach(key => localStorage.removeItem(key));
Logger.info(`🗑️ 已清除 ${keys.length} 个缓存项`);
return keys.length;
} catch (e) {
Logger.error('清除缓存失败: ', e);
return 0;
}
},
// 获取缓存统计信息
getStats() {
try {
const keys = Object.keys(localStorage).filter(key =>
key.startsWith(CACHE_CONFIG.prefix)
);
let totalSize = 0;
let validCount = 0;
let expiredCount = 0;
const now = Date.now();
keys.forEach(key => {
try {
const cached = localStorage.getItem(key);
totalSize += cached.length;
const cacheData = JSON.parse(cached);
if (now - cacheData.timestamp > cacheData.ttl) {
expiredCount++;
} else {
validCount++;
}
} catch (e) {
expiredCount++;
}
});
return {
total: keys.length,
valid: validCount,
expired: expiredCount,
size: totalSize
};
} catch (e) {
Logger.error('获取缓存统计失败: ', e);
return { total: 0, valid: 0, expired: 0, size: 0 };
}
}
};
// ==================== 状态提示框系统 ====================
const StatusNotifier = {
container: null,
messageQueue: [],
messageId: 0,
// 初始化状态提示框容器
init() {
if (!STATUS_CONFIG.enabled || this.container) return;
// 确保 DOM 已准备好
if (!document.body) {
setTimeout(() => this.init(), 50);
return;
}
try {
this.container = document.createElement('div');
this.container.id = 'njustStatusNotifier';
// 根据配置设置位置
const positions = {
'top-left': { top: '20px', left: '20px', flexDirection: 'column' },
'top-right': { top: '20px', right: '20px', flexDirection: 'column' },
'bottom-left': { bottom: '20px', left: '20px', flexDirection: 'column-reverse' },
'bottom-right': { bottom: '20px', right: '20px', flexDirection: 'column-reverse' }
};
const pos = positions[STATUS_CONFIG.position] || positions['top-right'];
this.container.style.cssText = `
position: fixed;
${Object.entries(pos).filter(([k]) => k !== 'flexDirection').map(([k, v]) => `${k}: ${v}`).join('; ')};
display: flex;
flex-direction: ${pos.flexDirection};
gap: 8px;
z-index: 9999;
pointer-events: none;
max-width: 350px;
`;
document.body.appendChild(this.container);
} catch (e) {
console.error('StatusNotifier 初始化失败: ', e);
this.container = null;
}
},
// 显示状态消息
show(message, type = 'info', duration = null) {
if (!STATUS_CONFIG.enabled || !UI_CONFIG.showNotifications) return;
try {
this.init();
// 确保容器已创建
if (!this.container) {
console.warn('StatusNotifier 容器未创建,跳过消息显示');
return;
}
// 如果是 loading 类型的消息,先隐藏之前的 loading 消息
if (type === 'loading') {
const existingLoadingMessages = this.messageQueue.filter(m => m.type === 'loading');
existingLoadingMessages.forEach(m => this.hideMessage(m.id));
}
const messageElement = this.createMessageElement(message, type);
const messageData = {
id: ++this.messageId,
element: messageElement,
type: type,
timestamp: Date.now()
};
this.messageQueue.push(messageData);
this.container.appendChild(messageElement);
// 限制同时显示的消息数量
this.limitMessages();
// 显示动画
requestAnimationFrame(() => {
if (messageElement.parentNode) {
messageElement.style.opacity = '1';
messageElement.style.transform = 'translateX(0)';
}
});
// 自动隐藏逻辑
if (STATUS_CONFIG.autoHide && type !== 'loading') {
const hideTime = duration || this.getHideDelay(type);
setTimeout(() => this.hideMessage(messageData.id), hideTime);
}
} catch (e) {
console.error('StatusNotifier 显示消息失败: ', e);
}
},
// 创建消息元素
createMessageElement(message, type) {
const icons = {
info: 'ℹ️',
success: '✅',
warning: '⚠️',
error: '❌',
loading: '🔄'
};
const colors = {
info: '#888',
success: '#888',
warning: '#888',
error: '#888',
loading: '#888'
};
const messageElement = document.createElement('div');
messageElement.style.cssText = `
background: ${colors[type] || colors.info};
color: white;
padding: 12px 16px;
border-radius: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
opacity: 0;
transform: translateX(${STATUS_CONFIG.position.includes('right') ? '20px' : '-20px'});
transition: all 0.3s ease;
pointer-events: auto;
line-height: 1.4;
cursor: pointer;
position: relative;
margin-bottom: 0;
`;
messageElement.innerHTML = `${icons[type] || icons.info} ${message}`;
// 点击关闭功能
messageElement.addEventListener('click', () => {
const messageData = this.messageQueue.find(m => m.element === messageElement);
if (messageData) {
this.hideMessage(messageData.id);
}
});
return messageElement;
},
// 获取不同类型消息的隐藏延迟
getHideDelay(type) {
const delays = {
info: STATUS_CONFIG.infoDelay || 2000, // info 消息显示更久
success: STATUS_CONFIG.hideDelay || 2000,
warning: STATUS_CONFIG.hideDelay || 2000,
error: STATUS_CONFIG.hideDelay || 2000,
loading: STATUS_CONFIG.hideDelay || 2000 // loading 消息不自动隐藏
};
return delays[type] || STATUS_CONFIG.hideDelay;
},
// 隐藏指定消息
hideMessage(messageId) {
const messageIndex = this.messageQueue.findIndex(m => m.id === messageId);
if (messageIndex === -1) return;
const messageData = this.messageQueue[messageIndex];
const element = messageData.element;
// 立即从队列中移除,避免 limitMessages 中的循环问题
this.messageQueue.splice(messageIndex, 1);
// 隐藏动画
element.style.opacity = '0';
element.style.transform = `translateX(${STATUS_CONFIG.position.includes('right') ? '20px' : '-20px'})`;
// 延迟移除 DOM 元素
setTimeout(() => {
if (element.parentNode) {
element.parentNode.removeChild(element);
}
}, 300);
},
// 限制同时显示的消息数量
limitMessages() {
// 避免无限循环: 只移除超出数量的消息,不使用 while 循环
if (this.messageQueue.length > STATUS_CONFIG.maxMessages) {
const excessCount = this.messageQueue.length - STATUS_CONFIG.maxMessages;
// 移除最旧的消息
for (let i = 0; i < excessCount; i++) {
if (this.messageQueue.length > 0) {
const oldestMessage = this.messageQueue[0];
this.hideMessage(oldestMessage.id);
}
}
}
},
// 隐藏所有消息
hide() {
this.messageQueue.forEach(messageData => {
this.hideMessage(messageData.id);
});
},
// 移除状态提示框
remove() {
if (this.container) {
this.container.remove();
this.container = null;
this.messageQueue = [];
}
}
};
// 状态提示框配置
const STATUS_CONFIG = {
enabled: true, // 是否显示状态提示
autoHide: true, // 是否自动隐藏
hideDelay: 2000, // 默认自动隐藏延迟 (毫秒)
infoDelay: 2000, // info 类型消息显示时间 (毫秒)
maxMessages: 5, // 同时显示的最大消息数量
position: 'top-right' // 位置: top-left, top-right, bottom-left, bottom-right
};
// 延迟初始化日志,避免在 DOM 未完全加载时出现问题
function initializeLogging() {
// 确保 DOM 已加载
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeLogging);
return;
}
// 延迟执行,避免与页面初始化冲突
setTimeout(() => {
try {
Logger.info('🚀 南理工教务增强助手已启动', {
debug: DEBUG_CONFIG.enabled ? `Level ${DEBUG_CONFIG.level}` : '关闭',
cache: CACHE_CONFIG.enabled ? `TTL ${CACHE_CONFIG.ttl}s` : '关闭'
});
// 显示缓存统计
if (DEBUG_CONFIG.enabled && DEBUG_CONFIG.showCache) {
const stats = CacheManager.getStats();
Logger.info('📊 缓存统计: ', {
总数: stats.total,
有效: stats.valid,
过期: stats.expired,
大小: (stats.size / 1024).toFixed(1) + 'KB'
});
}
} catch (e) {
console.error('初始化日志失败: ', e);
}
}, 100);
}
// 调用初始化
initializeLogging();
let courseCategoryMap = {};
let courseOutlineMap = {};
// 统一弹窗样式函数
function createUnifiedModal(title, content, type = 'info') {
// 移除可能存在的旧弹窗
const existingModal = document.getElementById('njustAssistantModal');
if (existingModal) {
existingModal.remove();
}
const container = document.createElement('div');
container.id = 'njustAssistantModal';
// 根据类型设置不同的渐变色
let gradientColor;
switch (type) {
case 'warning':
gradientColor = 'linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%)';
break;
case 'success':
gradientColor = 'linear-gradient(135deg, #28a745 0%, #20c997 100%)';
break;
case 'info':
default:
gradientColor = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
break;
}
container.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: ${gradientColor};
border: none;
border-radius: 15px;
padding: 0;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
z-index: 10000;
min-width: 200px;
max-width: 500px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
overflow: hidden;
animation: fadeIn 0.3s ease-out;
`;
container.innerHTML = `
<div id="dragHandle" style="
background: rgba(255,255,255,0.1);
padding: 15px 20px;
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(255,255,255,0.2);
">
<div style="color: white; font-weight: bold; font-size: 18px;">
🎓 ${title}
</div>
<span style="
cursor: pointer;
color: rgba(255,255,255,0.8);
font-size: 18px;
padding: 2px 6px;
border-radius: 4px;
transition: background-color 0.2s;
"
onclick="this.closest('div').parentElement.remove()"
onmouseover="this.style.backgroundColor='rgba(255,255,255,0.2)'"
onmouseout="this.style.backgroundColor='transparent'">✕</span>
</div>
<div style="
background: white;
padding: 25px;
">
${content}
<div style="
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #eee;
font-size: 12px;
color: #666;
line-height: 1.4;
text-align: center;
">
<div style="margin-bottom: 8px;">
<strong>请查看
<a href="https://enhance.njust.wiki" target="_blank" style="color: #007bff; text-decoration: none;">官方网站</a>
以获取使用说明</strong>
</div>
<div style="color: #ff6b6b; font-weight: bold; margin-bottom: 5px;">⚠️ 免责声明</div>
<div>本工具仅为学习交流使用,数据仅供参考。</div>
<div>请以教务处官网信息为准,使用本工具产生的任何后果均由用户自行承担。</div>
</div>
</div>
`;
// 添加 CSS 动画
if (!document.getElementById('njustAssistantStyles')) {
const style = document.createElement('style');
style.id = 'njustAssistantStyles';
style.textContent = `
@keyframes fadeIn {
from { opacity: 0; transform: translate(-50%, -50%) scale(0.9); }
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
`;
document.head.appendChild(style);
}
// 添加拖动功能
addDragFunctionality(container);
document.body.appendChild(container);
return container;
}
// 拖动功能
function addDragFunctionality(container) {
let isDragging = false;
let currentX, currentY, initialX, initialY;
let xOffset = 0, yOffset = 0;
const dragHandle = container.querySelector('#dragHandle');
function dragStart(e) {
if (e.type === "touchstart") {
initialX = e.touches[0].clientX - xOffset;
initialY = e.touches[0].clientY - yOffset;
} else {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
}
if (e.target === dragHandle || dragHandle.contains(e.target)) {
isDragging = true;
}
}
function dragEnd(e) {
initialX = currentX;
initialY = currentY;
isDragging = false;
}
function drag(e) {
if (isDragging) {
e.preventDefault();
if (e.type === "touchmove") {
currentX = e.touches[0].clientX - initialX;
currentY = e.touches[0].clientY - initialY;
} else {
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
}
xOffset = currentX;
yOffset = currentY;
container.style.transform = `translate(${currentX}px, ${currentY}px)`;
}
}
dragHandle.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
dragHandle.addEventListener('touchstart', dragStart, { passive: false });
document.addEventListener('touchmove', drag, { passive: false });
document.addEventListener('touchend', dragEnd, { passive: false });
}
// 检测强智科技页面
function checkQiangzhiPage() {
try {
const currentUrl = window.location.href;
const pageTitle = document.title || '';
Logger.debug('🔍 检测页面类型', {
URL: currentUrl,
标题: pageTitle
});
// 检测是否为强智科技页面且无法登录(不可用)
if (pageTitle.includes('强智科技教务系统概念版')) {
Logger.warn('⚠️ 检测到强智科技概念版页面,显示登录(不可用)引导');
const content = `
<div style="text-align: center; font-size: 16px; color: #333; margin-bottom: 20px; line-height: 1.6;">
<div style="font-size: 20px; margin-bottom: 15px;">🚫 该页面无法登录(不可用)</div>
<div style="margin-top: 10px;">请转向以下正确的登录(不可用)页面:</div>
</div>
<div style="text-align: center; margin: 20px 0;">
<div style="margin: 10px 0;">
<a href="https://www.njust.edu.cn/" target="_blank" style="
display: inline-block;
background: #28a745;
color: white;
padding: 12px 20px;
text-decoration: none;
border-radius: 8px;
margin: 5px;
font-weight: bold;
transition: background-color 0.2s;
" onmouseover="this.style.backgroundColor='#218838'" onmouseout="this.style.backgroundColor='#28a745'">
🏫 智慧理工登录(不可用)页面
</a>
</div>
<div style="margin: 10px 0;">
<a href="http://202.119.81.113:8080/" target="_blank" style="
display: inline-block;
background: #007bff;
color: white;
padding: 12px 20px;
text-decoration: none;
border-radius: 8px;
margin: 5px;
font-weight: bold;
transition: background-color 0.2s;
" onmouseover="this.style.backgroundColor='#0056b3'" onmouseout="this.style.backgroundColor='#007bff'">
🔗 教务处登录(不可用)页面
</a>
</div>
</div>
<div style="
margin-top: 15px;
padding: 10px;
background: #f8f9fa;
border-radius: 6px;
font-size: 14px;
color: #666;
text-align: center;
">
💡 提示:<br>
强智科技教务系统概念版是无法登陆的。<br>
请使用上述链接跳转到正确的登录(不可用)页面,<br>
登录(不可用)后可正常使用教务系统功能<br>
验证码区分大小写,大部分情况下均为小写
</div>
`;
try {
createUnifiedModal('南理工教务增强助手', content, 'warning');
} catch (e) {
Logger.error('❌ 创建强智科技页面提示弹窗失败:', e);
}
return true;
}
return false;
} catch (e) {
Logger.error('❌ 检测强智科技页面失败:', e);
return false;
}
}
function loadJSON(url) {
return new Promise((resolve, reject) => {
Logger.debug(`📡 请求数据: ${url}`);
// 尝试从缓存获取数据
const cachedData = CacheManager.get(url);
if (cachedData) {
Logger.debug(`🎯 使用缓存数据: ${url}`);
// 显示缓存命中状态
const fileName = url.includes('xxk') ? '选修课分类' : '课程大纲';
StatusNotifier.show(`从缓存读取${fileName}数据成功`, 'success');
resolve(cachedData);
return;
}
// 缓存未命中,发起网络请求
Logger.info(`🌐 发起网络请求: ${url}`);
const startTime = Date.now();
// 显示加载状态
const fileName = url.includes('xxk') ? '选修课分类' : '课程大纲';
// StatusNotifier.show(`正在从远程加载${fileName}数据...`, 'info', 0);
GM_xmlhttpRequest({
method: "GET",
url,
onload: function (response) {
const loadTime = Date.now() - startTime;
try {
const json = JSON.parse(response.responseText);
// 保存到缓存
const cached = CacheManager.set(url, json);
Logger.info(`✅ 请求成功: ${url}`, {
耗时: loadTime + 'ms',
大小: response.responseText.length + ' bytes',
缓存: cached ? '已保存' : '保存失败'
});
// 显示成功状态
StatusNotifier.show(`从远程加载${fileName}成功 (${loadTime}ms)`, 'success');
resolve(json);
} catch (e) {
Logger.error(`❌ JSON 解析失败: ${url}`, e);
StatusNotifier.show(`${fileName}数据解析失败`, 'error');
reject(e);
}
},
onerror: function (err) {
const loadTime = Date.now() - startTime;
Logger.error(`❌ 网络请求失败: ${url}`, {
耗时: loadTime + 'ms',
错误: err
});
StatusNotifier.show(`${fileName}数据加载失败`, 'error', 4000);
reject(err);
}
});
});
}
function buildCourseMaps(categoryList, outlineList) {
try {
Logger.debug('🔨 开始构建课程映射表');
let categoryCount = 0;
let outlineCount = 0;
// 安全处理分类数据
if (Array.isArray(categoryList)) {
categoryList.forEach(item => {
try {
if (item && item.course_code && item.category) {
courseCategoryMap[item.course_code.trim()] = item.category;
categoryCount++;
}
} catch (e) {
Logger.warn('⚠️ 处理分类数据项时出错:', e, item);
}
});
} else {
Logger.warn('⚠️ 分类数据不是数组格式:', typeof categoryList);
}
// 安全处理大纲数据
if (Array.isArray(outlineList)) {
outlineList.forEach(item => {
try {
if (item && item.course_code && item.id) {
courseOutlineMap[item.course_code.trim()] = item.id;
outlineCount++;
}
} catch (e) {
Logger.warn('⚠️ 处理大纲数据项时出错:', e, item);
}
});
} else {
Logger.warn('⚠️ 大纲数据不是数组格式:', typeof outlineList);
}
Logger.info('📋 课程映射表构建完成', {
选修课类别: categoryCount + '条',
课程大纲: outlineCount + '条',
总数据: (categoryCount + outlineCount) + '条'
});
} catch (e) {
Logger.error('❌ 构建课程映射表失败:', e);
// 确保映射表至少是空对象,避免后续访问出错
if (typeof courseCategoryMap !== 'object') courseCategoryMap = {};
if (typeof courseOutlineMap !== 'object') courseOutlineMap = {};
}
}
function createCreditSummaryWindow() {
try {
// 使用统一的弹窗样式,但保持原有的固定位置和拖动功能
const container = document.createElement('div');
container.id = 'creditSummaryWindow';
container.style.cssText = `
position: fixed;
top: 40px;
right: 40px;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 14px;
padding: 0;
box-shadow: 0 8px 32px rgba(0,0,0,0.13);
z-index: 9999;
min-width: 420px;
max-width: 520px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
overflow: hidden;
`;
container.innerHTML = `
<div id="creditDragHandle" style="
background: #f5f6fa;
padding: 14px 22px;
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #e0e0e0;
">
<div style="color: #333; font-weight: 600; font-size: 17px; letter-spacing: 1px;">
🎓 南理工教务增强助手
</div>
<span style="
cursor: pointer;
color: #888;
font-size: 18px;
padding: 2px 8px;
border-radius: 4px;
transition: background-color 0.2s;
"
onclick="this.closest('div').parentElement.remove()"
onmouseover="this.style.backgroundColor='#e0e0e0'"
onmouseout="this.style.backgroundColor='transparent'">✕</span>
</div>
<div style="
background: #fff;
padding: 18px 22px 10px 22px;
max-height: 540px;
overflow-y: auto;
">
<div id="creditSummary"></div>
<div style="
margin-top: 18px;
padding-top: 12px;
border-top: 1px solid #e0e0e0;
font-size: 13px;
color: #888;
line-height: 1.6;
text-align: left;
">
<div style="color: #e67e22; font-weight: 500; margin-bottom: 5px;">⚠️ 特别声明</div>
<div>选修课类别可能发生变化,仅供参考。<br>本工具可能因为教务处改版而不可靠,不对数据准确性负责</div>
<div style="margin-bottom: 8px;">
<span>请查看 <a href="https://enhance.njust.wiki" target="_blank" style="color: #007bff; text-decoration: none;">南理工教务增强助手官方网站</a> 以获取使用说明</span>
</div>
</div>
</div>
`;
// 添加拖动功能
let isDragging = false;
let currentX, currentY, initialX, initialY;
let xOffset = 0, yOffset = 0;
const dragHandle = container.querySelector('#creditDragHandle');
if (!dragHandle) {
Logger.warn('⚠️ 未找到拖拽句柄元素');
document.body.appendChild(container);
return container;
}
function dragStart(e) {
try {
if (e.type === "touchstart") {
initialX = e.touches[0].clientX - xOffset;
initialY = e.touches[0].clientY - yOffset;
} else {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
}
if (e.target === dragHandle || dragHandle.contains(e.target)) {
isDragging = true;
}
} catch (err) {
Logger.error('❌ 拖拽开始失败:', err);
}
}
function dragEnd(e) {
try {
initialX = currentX;
initialY = currentY;
isDragging = false;
} catch (err) {
Logger.error('❌ 拖拽结束失败:', err);
}
}
function drag(e) {
try {
if (isDragging) {
e.preventDefault();
if (e.type === "touchmove") {
currentX = e.touches[0].clientX - initialX;
currentY = e.touches[0].clientY - initialY;
} else {
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
}
xOffset = currentX;
yOffset = currentY;
container.style.transform = `translate(${currentX}px, ${currentY}px)`;
}
} catch (err) {
Logger.error('❌ 拖拽移动失败:', err);
}
}
dragHandle.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
dragHandle.addEventListener('touchstart', dragStart, { passive: false });
document.addEventListener('touchmove', drag, { passive: false });
document.addEventListener('touchend', dragEnd, { passive: false });
document.body.appendChild(container);
Logger.debug('✅ 学分统计弹窗创建完成');
return container;
} catch (e) {
Logger.error('❌ 创建学分统计弹窗失败:', e);
if (UI_CONFIG.showNotifications) {
StatusNotifier.show('创建学分统计弹窗失败', 'error', 3000);
}
return null;
}
}
function updateCreditSummary() {
try {
Logger.debug('📊 开始更新学分统计');
const creditSummaryDiv = document.getElementById('creditSummary');
if (!creditSummaryDiv) {
Logger.warn('⚠️ 未找到学分统计容器');
return;
}
const creditsByType = {}; // 按课程类型(通识教育课等)统计
const creditsByCategory = {}; // 按选修课类别统计
const tables = document.querySelectorAll('table');
tables.forEach(table => {
const rows = table.querySelectorAll('tr');
rows.forEach(row => {
const tds = row.querySelectorAll('td');
if (tds.length >= 11) {
const courseCode = tds[2].textContent.trim();
const credit = parseFloat(tds[6].textContent) || 0;
const courseType = tds[10].textContent.trim(); // 课程类型(通识教育课等)
// 从页面上已显示的类别信息中提取选修课类别
const categoryDiv = tds[2].querySelector('[data-category-inserted]');
let category = null;
if (categoryDiv) {
// 直接获取文本内容,因为现在只显示类别名称
category = categoryDiv.textContent.trim();
// 如果文本为空或者不是有效的类别,则设为 null
if (!category || category.length === 0) {
category = null;
}
}
// 按课程类型统计
if (courseType) {
if (!creditsByType[courseType]) {
creditsByType[courseType] = {
credits: 0,
count: 0
};
}
creditsByType[courseType].credits += credit;
creditsByType[courseType].count += 1;
}
// 按选修课类别统计
if (category) {
if (!creditsByCategory[category]) {
creditsByCategory[category] = {
credits: 0,
count: 0
};
}
creditsByCategory[category].credits += credit;
creditsByCategory[category].count += 1;
}
}
});
});
// 计算总计
const totalCreditsByType = Object.values(creditsByType).reduce((sum, data) => sum + data.credits, 0);
const totalCountByType = Object.values(creditsByType).reduce((sum, data) => sum + data.count, 0);
const totalCreditsByCategory = Object.values(creditsByCategory).reduce((sum, data) => sum + data.credits, 0);
const totalCountByCategory = Object.values(creditsByCategory).reduce((sum, data) => sum + data.count, 0);
Logger.debug('📈 学分统计结果', {
课程类型数: Object.keys(creditsByType).length,
选修课类别数: Object.keys(creditsByCategory).length,
总学分: totalCreditsByType.toFixed(1),
总课程数: totalCountByType
});
// 生成 HTML - 表格样式布局
let summaryHTML = '<div style="border-bottom: 1px solid #e0e0e0; margin-bottom: 12px; padding-bottom: 10px;">';
summaryHTML += '<div style="margin-bottom: 8px; font-size: 15px; color: #222; font-weight: 600; letter-spacing: 0.5px;">📊 按课程类型统计</div>';
// 总计行
summaryHTML += `<div style="display: grid; grid-template-columns: 2fr 1fr 1fr; gap: 6px; padding: 2px 0; align-items: center; background: #f7f7fa; border-radius: 4px; padding: 4px 6px; margin-bottom: 4px;">
<span style="color: #007bff; font-weight: 600; font-size: 13px; text-align: left;">总计</span>
<span style="font-weight: 600; color: #007bff; font-size: 13px; text-align: left;">${totalCreditsByType.toFixed(1)} 学分</span>
<span style="color: #007bff; font-weight: 600; font-size: 13px; text-align: left;">${totalCountByType} 门</span>
</div>`;
// 课程类型表格
summaryHTML += '<div style="display: grid; gap: 2px;">';
for (const [type, data] of Object.entries(creditsByType)) {
summaryHTML += `<div style="display: grid; grid-template-columns: 2fr 1fr 1fr; gap: 6px; padding: 2px 0; align-items: center;">
<span style="color: #444; font-weight: 400; font-size: 13px; text-align: left;">${type}</span>
<span style="font-weight: 400; color: #333; font-size: 13px; text-align: left;">${data.credits.toFixed(1)} 学分</span>
<span style="color: #888; font-size: 13px; text-align: left;">${data.count} 门</span>
</div>`;
}
summaryHTML += '</div>';
summaryHTML += '</div>';
if (Object.keys(creditsByCategory).length > 0) {
summaryHTML += '</div><div style="margin-top: 16px;">';
summaryHTML += '<div style="margin-bottom: 8px; font-size: 15px; color: #222; font-weight: 600; letter-spacing: 0.5px;">🏷️ 按选修课类别统计</div>';
// 总计行
summaryHTML += `<div style="display: grid; grid-template-columns: 2fr 1fr 1fr; gap: 6px; padding: 2px 0; align-items: center; background: #f7f7fa; border-radius: 4px; padding: 4px 6px; margin-bottom: 4px;">
<span style="color: 007bff; font-weight: 600; font-size: 13px; text-align: left;">总计</span>
<span style="font-weight: 600; color: #007bff; font-size: 13px; text-align: left;">${totalCreditsByCategory.toFixed(1)} 学分</span>
<span style="color: #007bff; font-weight: 600; font-size: 13px; text-align: left;">${totalCountByCategory} 门</span>
</div>`;
// 选修课类别表格
summaryHTML += '<div style="display: grid; gap: 2px;">';
for (const [category, data] of Object.entries(creditsByCategory)) {
summaryHTML += `<div style="display: grid; grid-template-columns: 2fr 1fr 1fr; gap: 6px; padding: 2px 0; align-items: center;">
<span style="color: #444; font-weight: 400; font-size: 13px; text-align: left;">${category}</span>
<span style="font-weight: 400; color: #333; font-size: 13px; text-align: left;">${data.credits.toFixed(1)} 学分</span>
<span style="color: #888; font-size: 13px; text-align: left;">${data.count} 门</span>
</div>`;
}
summaryHTML += '</div>';
}
summaryHTML += '</div>';
creditSummaryDiv.innerHTML = summaryHTML || '暂无数据';
Logger.debug('✅ 学分统计更新完成');
} catch (e) {
Logger.error('❌ 更新学分统计失败:', e);
const creditSummaryDiv = document.getElementById('creditSummary');
if (creditSummaryDiv) {
creditSummaryDiv.innerHTML = '<div style="color: #dc3545; padding: 10px; text-align: center;">❌ 学分统计更新失败</div>';
}
}
}
function processAllTables() {
try {
Logger.debug('🔍 开始处理页面表格');
const tables = document.querySelectorAll('table');
const isGradePage = window.location.pathname.includes('/njlgdx/kscj/cjcx_list');
const isSchedulePage = window.location.pathname.includes('xskb_list.do') &&
document.title.includes('学期理论课表');
Logger.debug(`📋 找到 ${tables.length} 个表格`, {
成绩页面: isGradePage,
课表页面: isSchedulePage
});
let processedTables = 0;
let processedRows = 0;
let enhancedCourses = 0;
tables.forEach(table => {
try {
// 如果是课表页面,只处理 id="dataList" 的表格
if (isSchedulePage && table.id !== 'dataList') {
Logger.debug('⏭️ 跳过非 dataList 表格');
return;
}
const rows = table.querySelectorAll('tr');
Logger.debug(`📋 处理表格 (${rows.length} 行)`, {
表格ID: table.id || '无 ID',
成绩页面: isGradePage,
课表页面: isSchedulePage
});
processedTables++;
rows.forEach(row => {
try {
const tds = row.querySelectorAll('td');
if (tds.length < 3) return;
processedRows++;
let courseCodeTd;
let courseCode;
if (isGradePage) {
courseCodeTd = tds[2]; // 成绩页面课程代码在第3列
courseCode = courseCodeTd ? courseCodeTd.textContent.trim() : '';
} else if (isSchedulePage) {
courseCodeTd = tds[1]; // 课表页面课程代码在第2列
courseCode = courseCodeTd ? courseCodeTd.textContent.trim() : '';
} else {
courseCodeTd = tds[1];
if (courseCodeTd && courseCodeTd.innerHTML) {
const parts = courseCodeTd.innerHTML.split('<br>');
if (parts.length === 2) {
courseCode = parts[1].trim();
} else {
return;
}
} else {
return;
}
}
if (!courseCode) return;
Logger.debug(`🔍 处理课程: ${courseCode}`);
let courseEnhanced = false;
// 插入类别
try {
if (courseCodeTd && !courseCodeTd.querySelector('[data-category-inserted]')) {
const category = courseCategoryMap[courseCode];
if (category) {
const catDiv = document.createElement('div');
catDiv.setAttribute('data-category-inserted', '1');
catDiv.style.color = '#28a745';
catDiv.style.fontWeight = 'bold';
catDiv.style.marginTop = '4px';
// 只显示类别名称,不显示前缀
catDiv.textContent = category;
courseCodeTd.appendChild(catDiv);
Logger.debug(`✅ 添加课程类别: ${category}`);
courseEnhanced = true;
}
}
} catch (e) {
Logger.warn('⚠️ 添加课程类别时出错:', e, courseCode);
}
// 插入老师说明(来自 title,仅在非成绩页面和非课表页面)
try {
if (!isGradePage && !isSchedulePage && courseCodeTd && courseCodeTd.title && !courseCodeTd.querySelector('[data-title-inserted]')) {
const titleDiv = document.createElement('div');
titleDiv.setAttribute('data-title-inserted', '1');
titleDiv.style.color = '#666';
titleDiv.style.fontSize = '13px';
titleDiv.style.marginTop = '4px';
titleDiv.style.fontStyle = 'italic';
titleDiv.textContent = `📌 老师说明: ${courseCodeTd.title}`;
courseCodeTd.appendChild(titleDiv);
Logger.debug(`📝 添加老师说明`);
courseEnhanced = true;
}
} catch (e) {
Logger.warn('⚠️ 添加老师说明时出错:', e, courseCode);
}
// 插入课程大纲链接
try {
if (courseCodeTd && !courseCodeTd.querySelector('[data-outline-inserted]')) {
const realId = courseOutlineMap[courseCode];
const outlineDiv = document.createElement('div');
outlineDiv.setAttribute('data-outline-inserted', '1');
outlineDiv.style.marginTop = '4px';
if (realId) {
const link = document.createElement('a');
link.href = `http://202.119.81.112:8080/kcxxAction.do?method=kcdgView&jx02id=${realId}&isentering=0`;
link.textContent = '📘 查看课程大纲';
link.target = '_blank';
link.style.color = '#0077cc';
outlineDiv.appendChild(link);
Logger.debug(`📘 添加课程大纲链接`);
courseEnhanced = true;
} else {
outlineDiv.textContent = '❌ 无大纲信息';
outlineDiv.style.color = 'gray';
Logger.debug(`❌ 无大纲信息`);
}
courseCodeTd.appendChild(outlineDiv);
}
} catch (e) {
Logger.warn('⚠️ 添加课程大纲链接时出错:', e, courseCode);
}
if (courseEnhanced) {
enhancedCourses++;
}
} catch (e) {
Logger.warn('⚠️ 处理表格行时出错:', e);
}
});
} catch (e) {
Logger.warn('⚠️ 处理表格时出错:', e);
}
});
// 输出处理统计
Logger.info('📊 表格处理统计', {
处理表格数: processedTables,
处理行数: processedRows,
增强课程数: enhancedCourses
});
// 更新学分统计(仅在成绩页面)
if (isGradePage) {
Logger.debug('📊 更新学分统计');
updateCreditSummary();
}
Logger.debug('✅ 表格处理完成');
} catch (e) {
Logger.error('❌ 处理页面表格失败:', e);
if (UI_CONFIG.showNotifications) {
StatusNotifier.show('页面表格处理失败', 'error', 3000);
}
}
}
// 统计追踪请求
/* function sendTrackingRequest() {
try {
// 发送追踪请求,用于统计使用情况
GM_xmlhttpRequest({
method: 'GET',
url: 'https://manual.njust.wiki/test.html?from=enhancer',
timeout: 5000,
onload: function () {
// 请求成功,不做任何处理
},
onerror: function () {
// 请求失败,静默处理
},
ontimeout: function () {
// 请求超时,静默处理
}
});
} catch (e) {
// 静默处理任何错误
}
} */
// 检测登录(不可用)错误页面并自动处理
function checkLoginErrorAndRefresh() {
try {
const pageTitle = document.title || '';
const pageContent = document.body ? document.body.textContent : '';
// 检测是否为登录(不可用)错误页面
const isLoginError = pageTitle.includes('出错页面') &&
(pageContent.includes('您登录(不可用)后过长时间没有操作') ||
pageContent.includes('您的用户名已经在别处登录(不可用)') ||
pageContent.includes('请重新输入帐号,密码后,继续操作'));
if (isLoginError) {
Logger.warn('⚠️ 检测到登录(不可用)超时或重复登录(不可用)错误页面');
// 显示用户提示
if (UI_CONFIG.showNotifications) {
StatusNotifier.show('检测到登录(不可用)超时,正在自动刷新登录(不可用)状态...', 'warning', 5000);
}
// 强制刷新登录(不可用)状态(忽略时间间隔限制)
performLoginRefresh(true);
return true;
}
return false;
} catch (e) {
Logger.error('❌ 检测登录(不可用)错误页面失败:', e);
return false;
}
}
// 执行登录(不可用)状态刷新
function performLoginRefresh(forceRefresh = false) {
const currentUrl = window.location.href;
try {
// 构建刷新 URL - 从当前 URL 提取基础部分
let baseUrl;
if (currentUrl.includes('njlgdx/')) {
baseUrl = currentUrl.substring(0, currentUrl.indexOf('njlgdx/'));
} else {
// 如果当前 URL 不包含 njlgdx,尝试从域名构建
const urlObj = new URL(currentUrl);
baseUrl = `${urlObj.protocol}//${urlObj.host}/`;
}
const refreshUrl = baseUrl + 'njlgdx/pyfa/kcdgxz';
Logger.info('🌐 准备使用隐藏 iframe 刷新登录(不可用)状态:', refreshUrl);
// 创建隐藏的 iframe 来加载刷新页面
const iframe = document.createElement('iframe');
iframe.style.cssText = `
position: absolute;
left: -9999px;
top: -9999px;
width: 1px;
height: 1px;
opacity: 0;
visibility: hidden;
border: none;
`;
iframe.src = refreshUrl;
// 添加加载完成监听器
iframe.onload = function() {
Logger.info('✅ 登录(不可用)状态刷新请求已完成');
if (forceRefresh && UI_CONFIG.showNotifications) {
StatusNotifier.show('登录(不可用)状态已刷新,请重新尝试操作', 'success', 3000);
}
// 延迟移除 iframe,确保请求完全处理
setTimeout(() => {
if (iframe.parentNode) {
iframe.parentNode.removeChild(iframe);
Logger.debug('🗑️ 隐藏 iframe 已清理');
}
}, 1000);
};
// 添加错误处理
iframe.onerror = function() {
Logger.warn('⚠️ 登录(不可用)状态刷新请求失败');
if (iframe.parentNode) {
iframe.parentNode.removeChild(iframe);
}
if (forceRefresh && UI_CONFIG.showNotifications) {
StatusNotifier.show('登录(不可用)状态刷新失败,请手动重新点击选课中心 - 课程总库', 'error', 5000);
}
};
// 将 iframe 添加到页面
document.body.appendChild(iframe);
// 设置超时清理,防止 iframe 长时间存在
setTimeout(() => {
if (iframe.parentNode) {
iframe.parentNode.removeChild(iframe);
Logger.debug('⏰ 超时清理隐藏 iframe');
}
}, 10000); // 10 秒超时
} catch (e) {
Logger.error('❌ 自动刷新登录(不可用)状态失败:', e);
if (forceRefresh && UI_CONFIG.showNotifications) {
StatusNotifier.show('登录(不可用)状态刷新失败,请手动重新登录(不可用)', 'error', 5000);
}
}
}
// 自动刷新登录(不可用)状态功能
function autoRefreshLoginStatus() {
try {
const currentUrl = window.location.href;
// 检查当前页面 URL 是否包含 njlgdx/framework/main.jsp
if (currentUrl.includes('njlgdx/framework/main.jsp')) {
// 防止频繁触发 - 检查上次刷新时间
const lastRefreshKey = 'njust_last_login_refresh';
const lastRefreshTime = localStorage.getItem(lastRefreshKey);
const now = Date.now();
const refreshInterval = 5 * 60 * 1000; // 5 分钟间隔
if (lastRefreshTime && (now - parseInt(lastRefreshTime)) < refreshInterval) {
Logger.debug('⏭️ 距离上次刷新不足5分钟,跳过本次刷新');
return;
}
Logger.info('🔄 检测到主框架页面,准备刷新登录(不可用)状态');
// 记录本次刷新时间
localStorage.setItem(lastRefreshKey, now.toString());
// 使用统一的刷新函数
performLoginRefresh(false);
}
} catch (e) {
Logger.error('❌ 自动刷新登录(不可用)状态检查失败:', e);
}
}
async function init() {
try {
Logger.info('🎯 开始执行主要逻辑');
// StatusNotifier.show('南理工教务助手正在启动...', 'info');
// 发送统计追踪请求
// sendTrackingRequest();
// 首先检测强智科技页面
if (checkQiangzhiPage()) {
Logger.info('🚪 强智科技页面检测完成,脚本退出');
return; // 如果是强智科技页面,显示提示后直接返回
}
// 检查是否需要自动刷新登录(不可用)状态
autoRefreshLoginStatus();
// 检测登录(不可用)错误页面并处理
checkLoginErrorAndRefresh();
Logger.info('📥 开始加载数据');
// StatusNotifier.show('正在加载课程数据...', 'loading');
const [categoryData, outlineData] = await Promise.all([
loadJSON(CATEGORY_URL),
loadJSON(OUTLINE_URL)
]);
Logger.info('✅ 数据加载完成,开始初始化功能');
// StatusNotifier.show('正在解析数据...', 'loading');
buildCourseMaps(categoryData, outlineData);
// 如果是成绩页面,创建悬浮窗
if (window.location.pathname.includes('/njlgdx/kscj/cjcx_list')) {
Logger.debug('📊 检测到成绩页面,创建学分统计窗口');
createCreditSummaryWindow();
}
Logger.debug('🔄 开始处理页面表格');
//StatusNotifier.show('正在处理页面表格...', 'loading');
processAllTables();
// StatusNotifier.show('页面表格处理完成', 'success', 2000);
Logger.debug('👀 启动页面变化监听器');
let isProcessing = false; // 防止死循环的标志
const observer = new MutationObserver((mutations) => {
try {
// 防止死循环:如果正在处理中,跳过
if (isProcessing) {
return;
}
// 检查是否有实际的内容变化(排除我们自己添加的元素)
const hasRelevantChanges = mutations.some(mutation => {
try {
// 如果是我们添加的标记元素,忽略
if (mutation.type === 'childList') {
for (let node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
// 如果是我们添加的标记元素,忽略这个变化
if (node.hasAttribute && (
node.hasAttribute('data-category-inserted') ||
node.hasAttribute('data-title-inserted') ||
node.hasAttribute('data-outline-inserted')
)) {
return false;
}
// 如果是表格相关的重要变化,才处理
if (node.tagName === 'TABLE' || node.tagName === 'TR' || node.tagName === 'TD') {
return true;
}
}
}
}
return false;
} catch (e) {
Logger.warn('⚠️ 检查页面变化时出错:', e);
return false;
}
});
if (hasRelevantChanges && !checkQiangzhiPage()) {
Logger.debug('🔄 检测到相关页面变化,重新处理表格');
isProcessing = true;
try {
// StatusNotifier.show('正在更新页面表格...', 'loading');
processAllTables();
// StatusNotifier.show('页面表格更新完成', 'success', 1500);
} catch (e) {
Logger.error('❌ 重新处理表格失败:', e);
} finally {
// 延迟重置标志,确保 DOM 修改完成
setTimeout(() => {
isProcessing = false;
}, 100);
}
}
} catch (e) {
Logger.error('❌ MutationObserver 回调函数执行失败:', e);
// 确保重置处理标志
isProcessing = false;
}
});
try {
observer.observe(document.body, { childList: true, subtree: true });
} catch (e) {
Logger.error('❌ 启动页面变化监听器失败:', e);
}
Logger.info('🎉 脚本初始化完成');
StatusNotifier.show('南理工教务增强助手加载成功!', 'success', 5000);
} catch (err) {
Logger.error('❌ 初始化失败:', err);
StatusNotifier.show('系统初始化失败', 'error', 5000);
}
}
setTimeout(init, 1000);
})();