// ==UserScript==
// @name 重庆工程学院继续教育挂课助手
// @namespace https://cqgcxy.wdjycj.com
// @version 12.2
// @description 文鼎教育在线 - 全自动跨课程挂课 | by wapokka
// @author wapokka
// @match https://cqgcxy.wdjycj.com/*
// @grant none
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
// ========== 显示重要提示 ==========
function showImportantTips() {
const tips = document.getElementById('study-important-tips');
if (tips) { tips.remove(); return; }
const div = document.createElement('div');
div.id = 'study-important-tips';
div.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400px;
background: linear-gradient(145deg, #1a1a2e, #16213e);
border: 2px solid #667eea;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(102, 126, 234, 0.4);
z-index: 999999;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
overflow: hidden;
`;
div.innerHTML = `
⚠️ 使用提示 - 请仔细阅读 ⚠️
🎯 全自动挂课
开启"自动换课"模式后,能够自动切换科目、自动播放,学完一科自动跳转下一科,全程无需手动操作!
⚡ 16倍速播放
支持最高16倍速视频播放!修改倍速后点击"应用"按钮即可生效,快进刷课时必备!
🎬 操作步骤
1. 进入视频课程页面
2. 点击播放视频
3. 勾选"自动换课"
4. 等待自动运行即可
📞 联系作者
微信:wapokka
办宽带用电信,电信宽带杠杠的!
`;
document.body.appendChild(div);
document.getElementById('study-close-tips').onclick = () => div.remove();
// 点击背景关闭
div.onclick = (e) => {
if (e.target === div) div.remove();
};
}
// ========== 配置 ==========
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.1 启动 - 全自动跨课程模式 | by wapokka');
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 = `
📚 挂课助手 v12.1 by wapokka
❗
⏳ 初始化...
倍速:
`;
document.body.appendChild(panel);
// 拖拽
let dragging = false, ox = 0, oy = 0;
panel.addEventListener('mousedown', e => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return;
dragging = true;
ox = e.clientX - panel.offsetLeft;
oy = e.clientY - panel.offsetTop;
});
document.addEventListener('mousemove', e => {
if (!dragging) return;
panel.style.left = e.clientX - ox + 'px';
panel.style.top = e.clientY - oy + 'px';
panel.style.right = 'auto';
panel.style.bottom = 'auto';
});
document.addEventListener('mouseup', () => { dragging = false; });
// 提示按钮
document.getElementById('sp-tips-btn').onclick = showImportantTips;
// 事件绑定
document.getElementById('sp-apply').onclick = () => {
const r = parseFloat(document.getElementById('sp-rate').value);
if (r > 0 && r <= 16) { CFG.speed = r; forceRate(r); }
};
document.getElementById('sp-enddelay').onchange = e => {
const v = parseInt(e.target.value);
if (v >= 3) CFG.afterEndDelay = v * 1000;
};
document.getElementById('sp-automode').onchange = e => {
isAutoMode = e.target.checked;
$log('自动换课:', isAutoMode);
};
document.getElementById('sp-stop').onclick = () => {
isStopped ? resumeAll() : stopAll();
};
document.getElementById('sp-next').onclick = () => {
if (isStopped) return;
has16x = false; endedFired = false;
const ok = clickNextLesson();
if (ok) {
setTimeout(() => {
clickPlay();
setTimeout(startPlayDetection, 500);
}, CFG.afterNextDelay);
}
};
}
if (document.body) {
createPanel();
// 首次使用自动弹出提示
if (!localStorage.getItem('study_tips_shown')) {
setTimeout(showImportantTips, 1000);
localStorage.setItem('study_tips_shown', 'true');
}
}
else document.addEventListener('DOMContentLoaded', createPanel);
})();