您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在合适的地方显示课程大纲、选修课类别及选修课学分情况,并自动刷新登录(不可用)状态。
// ==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); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址