// ==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}
功能:自动处理课程内容,支持视频、PPT、PDF
操作提示:
• 点击标题栏可拖动图框
• 点击"—"按钮可最小化图框
• 点击"↺"按钮可重置位置
${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(); })();