// ==UserScript== // @name 高教在线刷课助手(1.0.8 单实例修复版) // @namespace http://tampermonkey.net/ // @version 1.0.8 // @description 修复多图框问题,强化单实例控制 // @author Sweek // @match *://*.cqooc.com/* // @license GPLv3 // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // 单实例控制核心逻辑:先检查是否已有实例运行 const instanceKey = 'course_helper_single_instance_v108'; const existingInstance = GM_getValue(instanceKey, false); // 如果已有实例运行,则立即终止当前脚本 if (existingInstance) { console.log('高教在线刷课助手:已有实例运行,当前实例终止'); return; } // 严格清理所有可能存在的旧图框(无论是否本版本创建) const cleanOldFrames = () => { const oldFrames = document.querySelectorAll('#course-helper-frame'); if (oldFrames.length > 0) { console.log(`清理了${oldFrames.length}个残留图框`); oldFrames.forEach(el => el.remove()); } }; // 先执行清理操作 cleanOldFrames(); // 设置当前实例标识 GM_setValue(instanceKey, true); window.addEventListener('beforeunload', () => { GM_setValue(instanceKey, false); }); // 页面刷新/导航时也清理实例标识 window.addEventListener('popstate', () => { GM_setValue(instanceKey, false); }); // 淡蓝色系样式(保持不变) GM_addStyle(` #course-helper-frame { position: fixed !important; top: 20px !important; left: 20px !important; width: 420px !important; height: 540px !important; background: #ffffff !important; border: 1px solid #e0e8f0 !important; border-radius: 8px !important; box-shadow: 0 4px 20px rgba(74, 144, 226, 0.15) !important; z-index: 9999999 !important; overflow: hidden !important; font-family: "Segoe UI", "Microsoft YaHei", sans-serif !important; transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) !important; pointer-events: auto !important; } #frame-header { height: 50px !important; background: #4a90e2 !important; color: #ffffff !important; padding: 0 18px !important; display: flex !important; justify-content: space-between !important; align-items: center !important; cursor: move !important; user-select: none !important; pointer-events: auto !important; } #frame-title { font-size: 15px !important; font-weight: 600 !important; } #frame-version { font-size: 12px !important; opacity: 0.9 !important; margin-left: 8px !important; font-weight: normal !important; } #frame-controls { display: flex !important; gap: 8px !important; } .control-btn { width: 32px !important; height: 32px !important; border-radius: 4px !important; background: rgba(255, 255, 255, 0.2) !important; color: #ffffff !important; border: none !important; outline: none !important; cursor: pointer !important; font-size: 14px !important; display: flex !important; align-items: center !important; justify-content: center !important; transition: background 0.2s !important; pointer-events: auto !important; } .control-btn:hover { background: rgba(255, 255, 255, 0.3) !important; } #frame-tabs { height: 45px !important; display: flex !important; border-bottom: 1px solid #f0f5f7 !important; background: #f7f9fc !important; pointer-events: auto !important; } .frame-tab { flex: 1 !important; display: flex !important; align-items: center !important; justify-content: center !important; font-size: 14px !important; color: #666666 !important; cursor: pointer !important; transition: all 0.2s !important; user-select: none !important; pointer-events: auto !important; } .frame-tab.active { color: #4a90e2 !important; font-weight: 500 !important; border-bottom: 2px solid #4a90e2 !important; background: #ffffff !important; } #frame-content { height: calc(100% - 95px) !important; overflow: hidden !important; pointer-events: auto !important; } .frame-panel { height: 100% !important; overflow-y: auto !important; padding: 15px !important; display: none !important; pointer-events: auto !important; } .frame-panel.active { display: block !important; } #status-panel p { margin: 0 0 12px 0 !important; line-height: 1.6 !important; font-size: 14px !important; color: #333333 !important; } #status-panel strong { color: #4a90e2 !important; } #log-panel { font-size: 13px !important; } .log-item { padding: 8px 0 !important; margin: 0 !important; border-bottom: 1px dashed #f0f5f7 !important; color: #555555 !important; line-height: 1.5 !important; } .log-time { color: #999999 !important; margin-right: 8px !important; } .log-content { color: #333333 !important; } .frame-panel::-webkit-scrollbar { width: 6px !important; } .frame-panel::-webkit-scrollbar-track { background: #f0f5f7 !important; } .frame-panel::-webkit-scrollbar-thumb { background: #c0d0e0 !important; border-radius: 3px !important; } .frame-panel::-webkit-scrollbar-thumb:hover { background: #a0b8d0 !important; } #course-helper-frame.minimized { height: 50px !important; box-shadow: 0 2px 10px rgba(74, 144, 226, 0.1) !important; } #course-helper-frame.minimized #frame-tabs, #course-helper-frame.minimized #frame-content { display: none !important; } `); // 图框状态管理 const frameManager = { element: null, isDragging: false, dragOffset: { x: 0, y: 0 }, isMinimized: false, version: '1.0.8', // 初始化图框 initFrame() { // 二次检查并清理(防止极端情况下的残留) cleanOldFrames(); this.element = document.createElement('div'); this.element.id = 'course-helper-frame'; this.element.innerHTML = `
高教在线刷课助手 v${this.version}
状态信息
执行日志

图框已初始化完成

版本号:v${this.version}

功能:自动处理课程内容,支持视频、PPT、PDF

操作提示:

• 点击标题栏可拖动图框

• 点击"—"按钮可最小化图框

• 点击"↺"按钮可重置位置

[${this.getCurrentTime()}] 单实例修复版启动成功(v${this.version})
`; document.body.appendChild(this.element); this.bindEvents(); this.log('图框初始化完成,单实例控制生效'); }, // 绑定所有事件(保持不变) bindEvents() { const header = this.element.querySelector('#frame-header'); const minimizeBtn = this.element.querySelector('#minimize-btn'); const resetBtn = this.element.querySelector('#reset-btn'); const tabs = this.element.querySelectorAll('.frame-tab'); // 拖拽事件 header.addEventListener('mousedown', (e) => this.startDrag(e)); document.addEventListener('mousemove', (e) => this.handleDrag(e)); document.addEventListener('mouseup', () => this.stopDrag()); document.addEventListener('mouseleave', () => this.stopDrag()); // 最小化按钮 minimizeBtn.addEventListener('click', () => this.toggleMinimize()); // 重置位置 resetBtn.addEventListener('click', () => this.resetPosition()); // 标签切换 tabs.forEach(tab => { tab.addEventListener('click', () => this.switchTab(tab)); }); }, // 开始拖拽(保持不变) startDrag(e) { if (e.button !== 0) return; e.preventDefault(); e.stopPropagation(); const rect = this.element.getBoundingClientRect(); this.isDragging = true; this.dragOffset.x = e.clientX - rect.left; this.dragOffset.y = e.clientY - rect.top; this.element.style.boxShadow = '0 6px 25px rgba(74, 144, 226, 0.25)'; this.element.style.transform = 'scale(1.01)'; this.log('开始拖拽图框'); }, // 处理拖拽(保持不变) handleDrag(e) { if (!this.isDragging) return; e.preventDefault(); const newX = e.clientX - this.dragOffset.x; const newY = e.clientY - this.dragOffset.y; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const frameWidth = this.element.offsetWidth; const frameHeight = this.element.offsetHeight; const constrainedX = Math.max(0, Math.min(newX, viewportWidth - frameWidth)); const constrainedY = Math.max(0, Math.min(newY, viewportHeight - frameHeight)); this.element.style.left = `${constrainedX}px`; this.element.style.top = `${constrainedY}px`; this.element.style.transform = 'none'; }, // 停止拖拽(保持不变) stopDrag() { if (this.isDragging) { this.isDragging = false; this.element.style.boxShadow = '0 4px 20px rgba(74, 144, 226, 0.15)'; this.log('图框拖拽结束'); } }, // 切换最小化状态(保持不变) toggleMinimize() { this.isMinimized = !this.isMinimized; const btn = this.element.querySelector('#minimize-btn'); if (this.isMinimized) { this.element.classList.add('minimized'); btn.textContent = '□'; btn.title = '还原'; this.log('图框已最小化'); } else { this.element.classList.remove('minimized'); btn.textContent = '—'; btn.title = '最小化'; this.log('图框已还原'); } }, // 重置位置(保持不变) resetPosition() { this.element.style.top = '20px'; this.element.style.left = '20px'; this.element.style.transform = 'none'; this.log('图框位置已重置'); }, // 切换标签页(保持不变) switchTab(tab) { if (tab.classList.contains('active') || this.isMinimized) return; this.element.querySelectorAll('.frame-tab').forEach(t => t.classList.remove('active')); this.element.querySelectorAll('.frame-panel').forEach(panel => panel.classList.remove('active')); tab.classList.add('active'); const targetPanel = tab.dataset.panel; this.element.querySelector(`#${targetPanel}-panel`).classList.add('active'); this.log(`切换到【${tab.textContent}】面板`); }, // 工具方法:获取当前时间 getCurrentTime() { const date = new Date(); return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); }, // 日志记录(保持不变) log(message) { if (this.isMinimized) return; const logPanel = this.element.querySelector('#log-panel'); const logItem = document.createElement('div'); logItem.className = 'log-item'; logItem.innerHTML = ` [${this.getCurrentTime()}] ${message} `; logPanel.appendChild(logItem); logPanel.scrollTop = logPanel.scrollHeight; }, // 更新状态面板(保持不变) updateStatus(content) { if (this.isMinimized) return; const statusPanel = this.element.querySelector('#status-panel'); const contentArray = Array.isArray(content) ? content : [content]; statusPanel.innerHTML = contentArray.map(item => `

${item}

`).join(''); } }; // 刷课核心功能(保持不变) function getContentType() { const bodyText = document.body.innerText.toLowerCase(); if (bodyText.includes('ppt') || bodyText.includes('幻灯片')) return 'ppt'; if (bodyText.includes('video') || bodyText.includes('播放')) return 'video'; if (bodyText.includes('pdf') || bodyText.includes('文档')) return 'pdf'; return 'unknown'; } function handleVideo() { return new Promise(resolve => { frameManager.log('开始处理视频内容'); const video = document.querySelector('video'); if (!video) { frameManager.log('未找到视频元素,35秒后自动跳过'); let countdown = 35; const timer = setInterval(() => { countdown--; frameManager.updateStatus([ `内容类型:视频(未找到)`, `等待进度:${Math.round((35 - countdown)/35*100)}%`, `剩余时间:${countdown}秒`, `即将自动跳过至下一节` ]); if (countdown <= 0) { clearInterval(timer); frameManager.log('视频未找到,已跳过'); resolve(); } }, 1000); return; } video.playbackRate = 1.5; frameManager.log(`视频倍速已设置为:${video.playbackRate}x`); const playVideo = () => { if (video.paused && !video.ended) { video.play().catch(() => { video.muted = true; video.play().catch(err => frameManager.log(`播放失败:${err.message}`)); }); } }; playVideo(); video.addEventListener('pause', playVideo); const timer = setInterval(() => { if (video.ended) { clearInterval(timer); video.removeEventListener('pause', playVideo); frameManager.log('视频播放完成'); resolve(); } else { const progress = (video.currentTime / video.duration) * 100; frameManager.updateStatus([ `内容类型:视频`, `播放进度:${progress.toFixed(1)}%`, `当前时长:${formatTime(video.currentTime)}/${formatTime(video.duration)}`, `播放倍速:${video.playbackRate}x` ]); } }, 1000); }); } function handlePPT() { return new Promise(resolve => { const totalSeconds = 35; frameManager.log(`开始处理PPT内容,${totalSeconds}秒后完成`); let remaining = totalSeconds; const timer = setInterval(() => { remaining--; const progress = ((totalSeconds - remaining) / totalSeconds) * 100; frameManager.updateStatus([ `内容类型:PPT`, `处理进度:${progress.toFixed(1)}%`, `剩余时间:${remaining}秒` ]); if (remaining <= 0) { clearInterval(timer); frameManager.log('PPT内容处理完成'); resolve(); } }, 1000); }); } function handlePDF() { return new Promise(resolve => { const totalSeconds = 35; frameManager.log(`开始处理PDF内容,${totalSeconds}秒后完成`); let remaining = totalSeconds; const timer = setInterval(() => { remaining--; const progress = ((totalSeconds - remaining) / totalSeconds) * 100; frameManager.updateStatus([ `内容类型:PDF`, `处理进度:${progress.toFixed(1)}%`, `剩余时间:${remaining}秒` ]); if (remaining <= 0) { clearInterval(timer); frameManager.log('PDF内容处理完成'); resolve(); } }, 1000); }); } function formatTime(seconds) { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; } async function processCurrentContent() { const contentType = getContentType(); frameManager.log(`检测到内容类型:${contentType}`); switch (contentType) { case 'video': await handleVideo(); break; case 'ppt': await handlePPT(); break; case 'pdf': await handlePDF(); break; default: frameManager.log('未知内容类型,35秒后跳过'); frameManager.updateStatus([ `内容类型:未知`, `等待时间:35秒后自动跳过` ]); await new Promise(resolve => setTimeout(resolve, 35000)); break; } } function startTaskProcessing() { if (!window.location.pathname.includes('/course/detail/courseStudy')) { frameManager.updateStatus([ '未检测到课程学习页面', '请先进入"在学课程"的学习页面', `版本号:v${frameManager.version}` ]); frameManager.log('未检测到课程学习页面'); return; } const tasks = Array.from(document.querySelectorAll('.third-level-box')) .filter(el => !el.innerText.toLowerCase().includes('作业') && !el.innerText.toLowerCase().includes('测验') && !el.innerText.toLowerCase().includes('考试')); if (tasks.length === 0) { frameManager.updateStatus(['未找到可处理的课程内容', '请确认已进入课程学习页面']); frameManager.log('未找到可处理的任务'); return; } const activeIndex = tasks.findIndex(el => el.classList.contains('active')); const taskQueue = tasks.slice(activeIndex > -1 ? activeIndex : 0); frameManager.log(`共发现${tasks.length}个任务,将从第${activeIndex + 1}个开始处理`); frameManager.updateStatus([ `总任务数:${tasks.length}个`, `剩余任务:${taskQueue.length}个`, `处理中...` ]); const processNextTask = () => { if (taskQueue.length === 0) { frameManager.log('所有任务处理完成'); frameManager.updateStatus(['所有课程内容已处理完成!']); return; } const nextTask = taskQueue.shift(); const taskName = nextTask.innerText.trim().substring(0, 25) + (nextTask.innerText.length > 25 ? '...' : ''); frameManager.log(`准备处理任务:${taskName}`); frameManager.updateStatus([`即将处理:${taskName}`, `剩余任务:${taskQueue.length}个`]); nextTask.click(); setTimeout(async () => { await processCurrentContent(); setTimeout(processNextTask, 3000); }, 5000); }; processNextTask(); } // 初始化执行 function init() { if (document.readyState !== 'complete') { setTimeout(init, 500); return; } // 再次检查并清理(防止页面加载过程中残留) cleanOldFrames(); frameManager.initFrame(); setTimeout(() => { startTaskProcessing(); }, 2000); } // 启动初始化 init(); })();