// ==UserScript== // @name 重庆工程学院继续教育挂课助手 // @namespace https://cqgcxy.wdjycj.com // @version 12.0.0 // @description 文鼎教育在线 - 全自动跨课程挂课:学完一科→返回学习中心→跳转下一科→循环 // @author AutoStudy // @match https://cqgcxy.wdjycj.com/* // @grant none // @run-at document-end // ==/UserScript== (function () { 'use strict'; // ========== 配置 ========== const CFG = { speed: 16, afterEndDelay: 10000, // 播完后等几秒切下一节 afterNextDelay: 3000, // 切换下一节后等几秒点播放 playingCheckMs: 3000, // 播放确认时间 courseSwitchDelay: 3000, // 课程播完后等几秒换课 noVideoTimeout: 30000, // 无视频超时时间(毫秒) noProgressTimeout: 60000, // 有视频但无进度超时 debug: true, }; // ========== 状态 ========== let timer = null; let isStopped = false; let has16x = false; let endedFired = false; let playDetectTimer = null; let endCountdown = null; let nextCountdown = null; let courseSwitchTimer = null; let noVideoTimer = null; let lastCurrentTime = 0; let lastTimeAt = Date.now(); let isAutoMode = true; function $log(...a) { if (CFG.debug) console.log('[挂课]', ...a); } // ========== 停止/恢复 ========== function stopAll() { isStopped = true; has16x = false; clearAllTimers(); $log('🛑 停止'); const btn = document.getElementById('sp-stop'); if (btn) { btn.textContent = '▶ 恢复'; btn.style.background = '#388e3c'; } updatePanel('⏸ 已停止', '#888'); } function resumeAll() { isStopped = false; has16x = false; endedFired = false; clearAllTimers(); timer = setInterval(mainLoop, 800); $log('▶ 恢复'); const btn = document.getElementById('sp-stop'); if (btn) { btn.textContent = '⏸ 停止'; btn.style.background = '#d32f2f'; } // 恢复后自动点播放 setTimeout(() => { clickPlay(); setTimeout(startPlayDetection, 500); updatePanel('⏳ 播放中...', '#ff8a65'); }, 300); } function clearAllTimers() { [timer, playDetectTimer, endCountdown, nextCountdown, courseSwitchTimer, noVideoTimer].forEach(t => { if (t) { clearInterval(t); clearTimeout(t); } }); timer = playDetectTimer = endCountdown = nextCountdown = courseSwitchTimer = noVideoTimer = null; } // ========== 找 video ========== function findVideo() { for (const iframe of document.querySelectorAll('iframe')) { try { const doc = iframe.contentDocument || iframe.contentWindow.document; const v = doc && doc.querySelector('video'); if (v) return v; } catch (_) {} } return document.querySelector('video'); } // ========== 点击播放 ========== function clickPlay() { const v = findVideo(); if (v && v.play) { try { const p = v.play(); if (p && p.catch) p.catch(e => $log('v.play() 被拒绝:', e.message)); } catch(e) {} } const bigBtn = document.querySelector('.prism-big-play-btn'); if (bigBtn) { bigBtn.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true, view: window})); bigBtn.click(); $log('点击 .prism-big-play-btn'); } const cover = document.querySelector('.prism-cover'); if (cover) { cover.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true, view: window})); cover.click(); } } // ========== 点击下一节 ========== function clickNextLesson() { const spans = document.querySelectorAll('span.ok'); for (const span of spans) { if (span.textContent.trim() === '下一节') { span.click(); $log('点击: 下一节'); return true; } } $log('没有找到下一节按钮'); return false; } // ========== 强制倍速 ========== function forceRate(rate) { const v = findVideo(); if (!v || !v.src || v.readyState < 1) return; try { v.playbackRate = rate; if (Math.abs(v.playbackRate - rate) > 0.1) { Object.defineProperty(v, 'playbackRate', { value: rate, writable: true, configurable: true }); v.playbackRate = rate; } } catch (_) {} } // ========== 播放检测 ========== function startPlayDetection() { const v = findVideo(); if (!v || playDetectTimer) return; if (has16x) return; let consecutiveMs = 0; lastCurrentTime = v.currentTime || 0; lastTimeAt = Date.now(); playDetectTimer = setInterval(() => { if (isStopped || has16x) { clearInterval(playDetectTimer); playDetectTimer = null; return; } const curr = findVideo(); if (!curr) { clearInterval(playDetectTimer); playDetectTimer = null; return; } if (curr.paused) { consecutiveMs = 0; lastCurrentTime = curr.currentTime; lastTimeAt = Date.now(); return; } const now = Date.now(); const dt = now - lastTimeAt; const dTime = Math.abs(curr.currentTime - lastCurrentTime); lastCurrentTime = curr.currentTime; lastTimeAt = now; if (dTime >= (dt * 0.3 / 1000)) { consecutiveMs += dt; if (consecutiveMs >= CFG.playingCheckMs) { clearInterval(playDetectTimer); playDetectTimer = null; enable16x(); } } else { consecutiveMs = 0; } }, 250); } function enable16x() { if (has16x || isStopped) return; has16x = true; $log('🎉 播放确认,开启', CFG.speed, 'x'); forceRate(CFG.speed); updatePanel(`🚀 ${CFG.speed}x 挂课中`, '#81c784'); } // ========== 视频结束事件 ========== function onVideoEnded() { if (isStopped || endedFired) return; endedFired = true; has16x = false; $log('本节播完了'); updatePanel(`✅ 完成,等${CFG.afterEndDelay/1000}s下一节`, '#ff8a65'); if (endCountdown) clearTimeout(endCountdown); endCountdown = setTimeout(() => { if (isStopped) return; const ok = clickNextLesson(); if (ok) { endedFired = false; if (nextCountdown) clearTimeout(nextCountdown); nextCountdown = setTimeout(() => { if (isStopped) return; clickPlay(); setTimeout(startPlayDetection, 500); updatePanel('⏳ 播放中...', '#ff8a65'); }, CFG.afterNextDelay); } else { // 没有下一节,课程学完 $log('📚 没有下一节,课程学完,准备换课'); onCourseFinished(); } }, CFG.afterEndDelay); } // ========== 课程学完,切换下一课程 ========== function onCourseFinished() { if (!isAutoMode) { updatePanel('✅ 本课程完成!', '#81c784'); return; } $log('📚 onCourseFinished 被调用'); updatePanel('⏳ 课程完成,切换下一科...', '#ff8a65'); clearAllTimers(); if (courseSwitchTimer) clearTimeout(courseSwitchTimer); courseSwitchTimer = setTimeout(() => { if (isStopped) return; $log('🚀 执行 goToLearningCenter'); goToLearningCenter(); }, CFG.courseSwitchDelay); } // ========== 去学习中心 ========== function goToLearningCenter() { $log('📍 goToLearningCenter: 导航到学习中心'); window.location.href = 'https://cqgcxy.wdjycj.com/user-index'; } // ========== 在学习中心选择下一未完成课程 ========== function selectNextIncompleteCourse() { $log('📍 selectNextIncompleteCourse: 扫描当前学期课程...'); // 找当前学期容器 const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); let node, semesterContainer = null; while ((node = walker.nextNode())) { const txt = node.textContent || ''; if (txt.includes('当前学期') || txt.includes('第三学期')) { let el = node.parentElement; let depth = 0; while (el && el !== document.body && depth < 20) { const cards = el.querySelectorAll('.course-item'); if (cards.length > 0) { semesterContainer = el; $log('找到学期容器,课程数:', cards.length); break; } el = el.parentElement; depth++; } if (semesterContainer) break; } } if (!semesterContainer) { $log('未找到学期容器,使用body'); semesterContainer = document.body; } // 收集所有课程 const courses = []; const cards = semesterContainer.querySelectorAll('.course-item'); cards.forEach((card, idx) => { const img = card.querySelector('img'); const pctEl = card.querySelector('em'); const name = img ? img.alt : ''; const pctText = pctEl ? pctEl.textContent : '0'; const pct = parseFloat(pctText) || 0; const main = card.querySelector('.course-main'); $log(`课程${idx}: ${name} - ${pct}%`); if (name && pct >= 0) { courses.push({ name, pct, card, main, idx }); } }); if (courses.length === 0) { $log('未找到课程卡片'); updatePanel('❌ 未找到课程', '#f44336'); return; } // 排序:优先 >0% 且 <100%,然后 0% const incomplete = courses.filter(c => c.pct < 100); const inProgress = incomplete.filter(c => c.pct > 0); const notStarted = incomplete.filter(c => c.pct === 0); // 先按进度排序 inProgress(高的优先),再按顺序排 notStarted inProgress.sort((a, b) => b.pct - a.pct); notStarted.sort((a, b) => a.idx - b.idx); const sorted = [...inProgress, ...notStarted]; $log('排序后:', sorted.map(c => `${c.name}(${c.pct}%)`)); const target = sorted[0]; if (target) { $log('✅ 选择课程:', target.name, target.pct + '%'); updatePanel(`📚 进入: ${target.name}`, '#ff8a65'); if (target.main) { $log('点击 .course-main'); target.main.click(); } else { $log('点击 .course-item'); target.card.click(); } } else { $log('当前学期所有课程已完成!'); updatePanel('✅ 当前学期已全部完成!', '#81c784'); } } // ========== 在课程详情页点击学习按钮 ========== function clickStudyButton() { $log('📍 clickStudyButton: 查找学习按钮...'); // 橘黄色学习按钮 const studyBtn = document.querySelector('a.jion-study'); if (studyBtn) { $log('找到 a.jion-study,点击'); studyBtn.click(); return true; } // 备用:任意带"学习"文字的链接 const links = document.querySelectorAll('a'); for (const link of links) { if (link.textContent.trim() === '学习') { $log('找到学习链接,点击'); link.click(); return true; } } $log('未找到学习按钮'); return false; } // ========== 绑定视频事件 ========== function bindVideoEvents() { const v = findVideo(); if (!v || v._bound) return; v._bound = true; $log('绑定视频事件'); v.addEventListener('ended', onVideoEnded); v.addEventListener('play', () => { if (!isStopped && !has16x) { $log('video play 事件'); startPlayDetection(); } }); v.addEventListener('canplay', () => { if (!isStopped && !has16x) { $log('video canplay 事件'); startPlayDetection(); } }); v.addEventListener('ratechange', () => { if (has16x && v && Math.abs(v.playbackRate - CFG.speed) > 0.1) { forceRate(CFG.speed); } }); } // ========== 主循环 ========== function mainLoop() { if (isStopped) return; const v = findVideo(); if (v) { bindVideoEvents(); if (has16x && !v.paused) { forceRate(CFG.speed); if (v.duration > 0) { const pct = Math.round(v.currentTime / v.duration * 100); updatePanel(`🚀 ${CFG.speed}x | ${pct}%`, '#81c784'); } } } } // ========== 页面类型检测与处理 ========== function detectPageAndAct() { const url = window.location.href; $log('📍 页面URL:', url); if (url.includes('user-index') || url === 'https://cqgcxy.wdjycj.com/') { // 学习中心页面 $log('页面类型: 学习中心'); if (isAutoMode) { setTimeout(selectNextIncompleteCourse, 2000); } } else if (url.includes('course-detail')) { // 课程详情页 - 点击"学习"按钮 $log('页面类型: 课程详情'); setTimeout(clickStudyButton, 2000); } else if (url.includes('course-learn')) { // 视频学习页 - 开始挂课 $log('页面类型: 视频学习'); setTimeout(() => { const v = findVideo(); if (v && !v.paused && v.readyState >= 2) { startPlayDetection(); } else { clickPlay(); setTimeout(startPlayDetection, 1000); updatePanel('⏳ 播放中...', '#ff8a65'); } }, 1500); } } // ========== 启动 ========== function start() { $log('🚀 v12 启动 - 全自动跨课程模式'); clearAllTimers(); // 重置状态 has16x = false; endedFired = false; lastCurrentTime = 0; lastTimeAt = Date.now(); timer = setInterval(mainLoop, 800); // 检测页面类型并执行相应操作 detectPageAndAct(); } // ========== DOM Ready ========== if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', start); } else { start(); } // ========== SPA 路由变化 ========== let lastUrl = location.href; new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; $log('📍 路由变化:', location.href); clearAllTimers(); has16x = false; endedFired = false; setTimeout(start, 2000); } }).observe(document.body || document.documentElement, { subtree: true, childList: true }); // ========== UI面板 ========== function updatePanel(text, color) { const el = document.getElementById('sp-status'); if (el) { el.textContent = text; el.style.color = color || '#fff'; } } function createPanel() { if (document.getElementById('study-panel')) return; const panel = document.createElement('div'); panel.id = 'study-panel'; panel.style.cssText = ` position:fixed;bottom:20px;right:20px;z-index:99999; background:rgba(25,25,25,0.95);color:#fff;border-radius:10px; padding:12px 16px;font-size:13px;font-family:-apple-system,sans-serif; box-shadow:0 4px 20px rgba(0,0,0,0.5);min-width:220px; user-select:none;cursor:move; `; panel.innerHTML = `