// ==UserScript==
// @name 庆阳继续教育自动看视频
// @namespace https://qingyang.zgzjzj.com/
// @version 4.3
// @description 自动播放庆阳专技继续教育平台视频,支持多页小节、后台播放、静音、自动小节切换、弹窗处理、防误跳
// @author AutoScript
// @match https://qingyang.zgzjzj.com/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
// ==================== 后台播放限制解除 ====================
// @run-at document-start 确保在页面脚本之前执行
function bypassBackgroundRestriction() {
// 1. 覆盖 document.hidden / visibilityState
const overrides = {
hidden: { get: () => false },
visibilityState: { get: () => 'visible' },
webkitHidden: { get: () => false },
webkitVisibilityState: { get: () => 'visible' },
};
for (const [prop, desc] of Object.entries(overrides)) {
try { Object.defineProperty(document, prop, { ...desc, configurable: true }); }
catch (e) { /* 只读属性 */ }
}
// 2. 捕获阶段拦截所有可见性/焦点事件
const block = ['visibilitychange', 'webkitvisibilitychange', 'blur', 'pagehide'];
block.forEach(evt => {
window.addEventListener(evt, function (e) {
e.stopImmediatePropagation();
e.stopPropagation();
}, true);
document.addEventListener(evt, function (e) {
e.stopImmediatePropagation();
e.stopPropagation();
}, true);
});
}
bypassBackgroundRestriction();
// ==================== 全局静音 ====================
let globalMuted = true;
function muteAll(mute) {
globalMuted = mute;
document.querySelectorAll('video, audio').forEach(el => {
el.muted = mute;
if (mute) el.volume = 0;
});
// videojs 实例
try {
document.querySelectorAll('.video-js').forEach(el => {
if (el.player && el.player.muted) el.player.muted(mute);
});
} catch (e) { /* ignore */ }
}
// 监听新增的 media 元素
new MutationObserver(() => {
if (globalMuted) {
document.querySelectorAll('video:not([muted="true"]), audio:not([muted="true"])').forEach(el => {
el.muted = true;
el.volume = 0;
});
}
}).observe(document.documentElement, { childList: true, subtree: true });
// ==================== 配置 ====================
const CONFIG = {
stepDelay: 2000,
pollInterval: 5000,
sectionTimeout: 20 * 60 * 1000,
minPlayRatio: 0.5, // 小节至少播放视频的50%才认为完成
minPlaySeconds: 60, // 或者至少播放60秒
autoStartPlay: true,
debug: true,
};
let isRunning = false;
let shouldStop = false;
function log(...args) {
if (CONFIG.debug) console.log('[学习助手]', ...args);
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
function getPageType() {
const url = location.href;
if (url.includes('/my-all-course')) return 'courseList';
if (url.includes('/learncenter/play') || url.includes('/learncenter/study')) return 'playPage';
// DOM 兜底检测(URL 变体时更可靠)
if (document.querySelector('.class-catlog') || document.querySelector('#playcourse')) return 'playPage';
if (document.querySelector('.buyCourse_classAble')) return 'courseList';
return 'unknown';
}
// ==================== UI ====================
function createPanel(pageType) {
const old = document.getElementById('auto-study-panel');
if (old) old.remove();
const panel = document.createElement('div');
panel.id = 'auto-study-panel';
panel.innerHTML =
`
🎓 自动学习助手
${pageType === 'courseList' ? '课程列表—点开始' : '就绪,即将自动开始'}
后台播放已启用
`;
document.body.appendChild(panel);
document.getElementById('as-start').onclick = function () {
shouldStop = false; isRunning = false;
pageType === 'playPage' ? startPlayPage() : startCourseList();
};
document.getElementById('as-stop').onclick = function () {
shouldStop = true; isRunning = false; setStatus('已停止');
};
document.getElementById('as-mute').onclick = function () {
muteAll(!globalMuted);
this.textContent = globalMuted ? '🔇 静音中' : '🔊 有声音';
};
// 初始静音
setTimeout(() => muteAll(true), 1000);
}
function setStatus(msg) { var el = document.getElementById('as-status'); if (el) el.textContent = msg; log(msg); }
function setInfo(msg) { var el = document.getElementById('as-info'); if (el) el.textContent = msg; }
// ==================== 视频控制 ====================
function getVideoEl() {
return document.querySelector('#playcourse video, .video-js video, video.vjs-tech');
}
function playVideo() {
var v = getVideoEl();
if (!v) return;
try {
if (v.paused || v.ended) {
v.play().catch(function () {
var btn = document.querySelector('.vjs-big-play-button');
if (btn) btn.click();
});
}
} catch (e) { /* ignore */ }
}
// ==================== 小节解析 ====================
function getAllSections() {
var all = [];
var chapters = document.querySelectorAll('.class-catlog > ul > li');
chapters.forEach(function (ch) {
var sections = ch.querySelectorAll(':scope > ul > li');
sections.forEach(function (s) { all.push(s); });
});
return all;
}
function getSectionElement(index) {
var all = getAllSections();
return all[index] || null;
}
function getSectionName(li) {
var name = li.querySelector('.classname');
var num = li.querySelector('.sectionNum');
return (num ? num.textContent.trim() : '') + ' ' + (name ? name.textContent.trim() : '');
}
function isSectionFinished(li) {
return li.classList.contains('finished');
}
// ==================== 分页控制 ====================
function getPaginationInfo() {
// 优先在 .pagination 内查找(视频播放页),否则全局查找(课程列表页)
var pager = document.querySelector('.pagination .el-pagination') || document.querySelector('.el-pagination');
if (!pager) return null;
var pagerItems = pager.querySelectorAll('.el-pager .number');
var totalPages = pagerItems.length;
if (totalPages <= 0) return null;
var currentPage = 1;
pagerItems.forEach(function (item) {
if (item.classList.contains('active')) {
currentPage = parseInt(item.textContent.trim());
}
});
var nextBtn = pager.querySelector('.btn-next');
var hasNext = nextBtn && !nextBtn.disabled;
return { currentPage: currentPage, totalPages: totalPages, hasNext: hasNext };
}
async function goToNextPage() {
var pager = document.querySelector('.pagination .el-pagination') || document.querySelector('.el-pagination');
if (!pager) { log('未找到分页组件'); return false; }
var nextBtn = pager.querySelector('.btn-next');
if (!nextBtn || nextBtn.disabled) { log('没有下一页或按钮禁用'); return false; }
// 记录当前页码和小节数量,用于判断页面是否真正切换
var oldPage = getPaginationInfo().currentPage;
var oldSectionCount = getAllSections().length;
nextBtn.click();
log('点击下一页 (从第' + oldPage + '页)');
// 等待分页状态变化(active 页码改变)或超时
var changed = false;
for (var w = 0; w < 30; w++) { // 最多等30秒
await sleep(1000);
var newPag = getPaginationInfo();
if (newPag && newPag.currentPage !== oldPage) {
changed = true;
log('页码已切换到第' + newPag.currentPage + '页');
break;
}
}
if (!changed) {
log('分页切换超时,尝试继续');
}
// 等待小节内容刷新(DOM 变化)
await sleep(2000);
try {
await waitForEl('.class-catlog > ul > li', 15000);
} catch (e) {
log('等待小节元素超时');
}
// 再等2秒让视频播放器初始化
await sleep(2000);
log('页面加载完成,准备播放');
return true;
}
// ==================== 时间解析 ====================
function parseRemainingMinutes() {
var el = document.querySelector('.course-progress');
if (!el) return -1;
var text = el.textContent.replace(/\s+/g, ' '); // 把换行符等统一为空格
var m = text.match(/还需观看\s*(\d+)\s*分钟/);
if (m) return parseInt(m[1]);
// 备选:尝试匹配"还需观看 XX 分钟"格式
var m2 = text.match(/还需观看\s*(\d+)/);
if (m2) return parseInt(m2[1]);
return -1;
}
function parseWatchedMinutes() {
var el = document.querySelector('.course-progress');
if (!el) return 0;
var text = el.textContent.replace(/\s+/g, ' ');
var m = text.match(/已累计观看\s*(\d+)\s*分钟/);
if (m) return parseInt(m[1]);
var m2 = text.match(/已累计观看\s*(\d+)/);
if (m2) return parseInt(m2[1]);
return 0;
}
function parseMaxTime() {
var els = document.querySelectorAll('.timelength');
for (var i = 0; i < els.length; i++) {
var t = els[i].textContent;
if (t.indexOf('最大观看') >= 0 || t.indexOf('观看时长') >= 0) {
var m = t.match(/(\d+)/);
if (m) return parseInt(m[1]);
}
}
return -1;
}
// ==================== 课程内容反馈浮窗 ====================
function handleFeedbackBox() {
var fb = document.querySelector('.feedBackBox');
if (!fb) return false;
if (fb.offsetHeight < 5) return false;
// 点击关闭图标 h3 img[src*="cancel"]
var closeImg = fb.querySelector('h3 img[src*="cancel"]');
if (closeImg) { closeImg.click(); log('已关闭反馈浮窗'); return true; }
return false;
}
// ==================== 弹窗处理 ====================
async function handleDialogs() {
var dialogs = document.querySelectorAll('.el-dialog__wrapper');
for (var d = 0; d < dialogs.length; d++) {
var dlg = dialogs[d];
if (dlg.offsetHeight < 5) continue;
var style = window.getComputedStyle(dlg);
if (style.display === 'none') continue;
var title = dlg.querySelector('.el-dialog__title');
var titleText = title ? title.textContent.trim() : '';
// 视频抓拍/验证码
if (titleText.indexOf('视频抓拍') >= 0 || titleText.indexOf('验证码') >= 0) {
setStatus('⚠ 需要手动验证码!');
await sleep(30000);
continue;
}
// 随堂练习
if (titleText.indexOf('随堂练习') >= 0 || titleText.indexOf('练习') >= 0) {
await closeQuiz(dlg);
}
// 评价 → "以后再评"
if (titleText.indexOf('评价') >= 0 || titleText.indexOf('反馈') >= 0 || titleText.indexOf('感受') >= 0) {
var later = dlg.querySelector('.el-button.normal, .dialog-footer .el-button--default');
if (later && later.textContent.indexOf('以后再评') >= 0) {
later.click(); await sleep(800);
} else {
var cls = dlg.querySelector('.el-dialog__close, .el-icon-close');
if (cls) { cls.click(); await sleep(500); }
}
}
// 提示/通知
if (titleText.indexOf('提示') >= 0 || titleText.indexOf('通知') >= 0) {
var closeBtn = dlg.querySelector('.el-dialog__close, .el-icon-close');
if (closeBtn) { closeBtn.click(); await sleep(500); }
}
// 证书
if (titleText.indexOf('证书') >= 0) {
var cb = dlg.querySelector('.el-dialog__close, .el-icon-close');
if (cb) { cb.click(); await sleep(500); }
}
// 问卷/调研
if (titleText.indexOf('问卷') >= 0 || titleText.indexOf('调研') >= 0) {
var cancel = dlg.querySelector('.el-button.cancel, .dialog-footer .el-button--default');
if (cancel) { cancel.click(); await sleep(800); }
}
}
}
async function closeQuiz(dlg) {
try {
var cls = dlg.querySelector('.el-dialog__headerbtn, .el-icon-close');
if (cls) { cls.click(); await sleep(500); return; }
var opts = dlg.querySelectorAll('.el-radio, .el-checkbox');
if (opts.length > 0) { opts[0].click(); await sleep(300); }
var confirm = dlg.querySelector('.el-button--primary');
if (confirm) { confirm.click(); await sleep(500); }
} catch (e) { /* ignore */ }
}
// ==================== 播放小节 ====================
async function playSection(sectionIndex) {
var sec = getSectionElement(sectionIndex);
if (!sec) return;
var name = getSectionName(sec);
log('--- 小节 ' + (sectionIndex + 1) + ': ' + name + ' ---');
setInfo('播放: ' + name);
var playBtn = sec.querySelector('.play-btn');
if (playBtn) playBtn.click(); else sec.click();
await sleep(CONFIG.stepDelay);
await waitForVideo();
playVideo();
var startTime = Date.now();
while (!shouldStop) {
await sleep(CONFIG.pollInterval);
handleFeedbackBox();
await handleDialogs();
var v = getVideoEl();
if (v && v.paused) playVideo();
// 每次循环都读取"还需观看"时间
var rem = parseRemainingMinutes();
var w = parseWatchedMinutes();
var playPct = 0;
if (v && v.duration > 0) {
playPct = v.currentTime / v.duration * 100;
}
// 更新状态显示
setStatus('小节' + (sectionIndex + 1) + ' | 已看' + w + '分 剩' + rem + '分');
setInfo('播放: ' + name + ' | ' + Math.floor(playPct) + '%');
// ---- 完成判断 ----
var completed = false;
var reason = '';
// 最优先:还需观看时间为0 → 直接完成
if (rem === 0) {
completed = true;
reason = '所需时间已满足(还需0分钟)';
}
// 视频真正播放完毕
if (!completed && v && v.duration > 0) {
if (v.ended) {
completed = true;
reason = '视频已播放完毕(ended)';
} else if (v.currentTime >= v.duration - 2) {
completed = true;
reason = '视频即将播完';
} else if (playPct >= 95) {
completed = true;
reason = '播放进度已达' + Math.floor(playPct) + '%';
}
}
if (completed) {
log('小节 ' + (sectionIndex + 1) + ' 完成! 原因: ' + reason);
break;
}
// 超时保护
if (Date.now() - startTime > CONFIG.sectionTimeout) {
log('小节 ' + (sectionIndex + 1) + ' 超时,跳过');
break;
}
}
setInfo('等待...');
await sleep(CONFIG.stepDelay);
}
async function waitForVideo() {
return new Promise(function (resolve) {
var start = Date.now();
var check = function () {
var v = getVideoEl();
if (v && v.duration > 0 && v.readyState >= 2) return resolve();
if (Date.now() - start > 20000) return resolve();
setTimeout(check, 500);
};
check();
});
}
// ==================== 播放页主逻辑 ====================
async function startPlayPage() {
if (isRunning) return;
isRunning = true;
shouldStop = false;
log('========================================');
log('开始自动学习');
log('========================================');
try {
await sleep(3000);
await waitForEl('.class-catlog', 20000);
await sleep(2000);
var pag = getPaginationInfo();
var totalPages = pag ? pag.totalPages : 1;
log('分页信息: 共' + totalPages + '页');
// 第一轮:按顺序播放所有页的所有小节
log('--- 第一轮:遍历所有页 ---');
for (var pg = 1; pg <= totalPages; pg++) {
if (shouldStop) break;
// 如果不在目标页,先导航过去
if (pg > 1) {
// 到了新的一页后重新获取分页状态
pag = getPaginationInfo();
}
var allSections = getAllSections();
var sectionCount = allSections.length;
log('第' + pg + '页: 共' + sectionCount + '个小节');
setStatus('第' + pg + '/' + totalPages + '页 | 共' + sectionCount + '小节');
if (sectionCount === 0) {
log('第' + pg + '页无小节,跳过');
continue;
}
for (var i = 0; i < sectionCount; i++) {
log(' [P' + pg + '-' + (i + 1) + '] ' + getSectionName(allSections[i]) + ' - ' +
(isSectionFinished(allSections[i]) ? '已完成' : '待播放'));
}
// 播放当前页的所有小节
for (var i = 0; i < sectionCount; i++) {
if (shouldStop) break;
var rem = parseRemainingMinutes();
var w = parseWatchedMinutes();
setStatus('第' + pg + '页 播放 ' + (i + 1) + '/' + sectionCount + ' | 已看' + w + '分 剩' + rem + '分');
await playSection(i);
}
// 当前页播放完毕,尝试翻到下一页
if (!shouldStop && pg < totalPages) {
log('第' + pg + '页完成,翻到第' + (pg + 1) + '页');
setStatus('翻页: 第' + pg + '→' + (pg + 1) + '页');
var success = await goToNextPage();
if (!success) {
log('翻页失败,跳过后续页面');
break;
}
}
}
if (shouldStop) { setStatus('已停止'); return; }
// 第二轮:补时长,直到"还需观看"时间为0
var remaining = parseRemainingMinutes();
log('第一轮完成: 还需' + remaining + '分');
if (remaining > 0) {
log('--- 第二轮:补时长,从第1页第1节开始 ---');
// 先回到第1页
await goToPage1();
// 从第一节开始循环播放,直到时间满足
while (!shouldStop && remaining > 0) {
setStatus('补时长: 还需' + remaining + '分 | 从第1节重播');
// 点击第一节
var firstSec = getSectionElement(0);
if (firstSec) {
var playBtn = firstSec.querySelector('.play-btn');
if (playBtn) playBtn.click(); else firstSec.click();
}
await sleep(2000);
await waitForVideo();
playVideo();
// 等待播放直到时间满足或视频播放完毕
var retryStart = Date.now();
while (!shouldStop) {
await sleep(CONFIG.pollInterval);
handleFeedbackBox();
await handleDialogs();
var v = getVideoEl();
if (v && v.paused) playVideo();
// 检查"还需观看"时间
remaining = parseRemainingMinutes();
setStatus('补时长中 | 还需' + remaining + '分');
// 时间满足了就停
if (remaining <= 0) {
log('时间已满足! 还需' + remaining + '分');
break;
}
// 视频播放完毕了就换下一个视频继续
if (v && (v.ended || v.currentTime >= v.duration - 2)) {
log('视频播放完毕,继续播放');
break;
}
// 超时保护
if (Date.now() - retryStart > CONFIG.sectionTimeout) {
log('单个视频补时长超时');
break;
}
}
}
}
setStatus('✅ 本课程完成!即将跳转下一门...');
setInfo('本课程完成,3秒后返回列表');
await sleep(3000);
await handleDialogs();
// 自动跳回列表页,列表页会重新扫描找到下一个未完成的课程
if (!shouldStop) {
window.location.href = '/learncenter/my-all-course';
}
} catch (e) {
log('出错:', e.message);
setStatus('❌ ' + e.message);
} finally {
isRunning = false;
}
}
// 回到第1页(补时长用)
async function goToPage1() {
var pag = getPaginationInfo();
if (!pag || pag.currentPage === 1) return;
// 通过点击页码跳转到第1页
var pager = document.querySelector('.pagination .el-pagination') || document.querySelector('.el-pagination');
if (!pager) return;
var page1Btn = pager.querySelector('.el-pager .number');
if (page1Btn && !page1Btn.classList.contains('active')) {
var oldPage = pag.currentPage;
page1Btn.click();
log('跳回第1页 (从第' + oldPage + '页)');
// 等待页码切换
for (var w = 0; w < 20; w++) {
await sleep(1000);
var newPag = getPaginationInfo();
if (newPag && newPag.currentPage === 1) break;
}
await sleep(2000);
try { await waitForEl('.class-catlog > ul > li', 15000); } catch (e) {}
await sleep(2000);
}
}
// ==================== 课程列表页 ====================
async function startCourseList() {
setStatus('扫描课程...');
await sleep(2000);
var pag = getPaginationInfo();
var totalPages = pag ? pag.totalPages : 1;
log('课程列表: 共' + totalPages + '页');
// 逐页扫描,找到第一个未完成的课程并进入
for (var pg = 1; pg <= totalPages; pg++) {
if (shouldStop) break;
if (pg > 1) {
// 翻到下一页
var pager = document.querySelector('.pagination .el-pagination') || document.querySelector('.el-pagination');
if (pager) {
var nextBtn = pager.querySelector('.btn-next');
if (!nextBtn || nextBtn.disabled) {
log('课程列表没有下一页了');
break;
}
var oldPage = getPaginationInfo().currentPage;
nextBtn.click();
log('课程列表翻页: ' + oldPage + ' → ' + (pg));
// 等待页码切换
for (var w = 0; w < 20; w++) {
await sleep(1000);
var newPag = getPaginationInfo();
if (newPag && newPag.currentPage !== oldPage) break;
}
await sleep(2000);
try { await waitForEl('.buyCourse_listItem', 10000); } catch (e) {}
await sleep(1000);
}
}
// 扫描当前页的课程
var allItems = document.querySelectorAll('.buyCourse_listItem');
log('第' + pg + '页: 找到 ' + allItems.length + ' 个课程卡片');
var courses = [];
allItems.forEach(function (item, idx) {
var btns = item.querySelectorAll('.buyCourse_classAble .el-button');
var studyBtn = null;
btns.forEach(function (btn) {
var txt = btn.textContent.trim();
var isStudy = (txt.indexOf('学习') >= 0 || txt.indexOf('学 习') >= 0);
var isExam = (txt.indexOf('考试') >= 0);
if (isStudy && !isExam) {
studyBtn = btn;
}
});
var nameEl = item.querySelector('.buyCourse_itemTitle');
var name = nameEl ? nameEl.textContent.trim().substring(0, 25) : '课程';
var progress = 0;
var progressbar = item.querySelector('[role="progressbar"]');
if (progressbar) {
var val = progressbar.getAttribute('aria-valuenow');
if (val) progress = parseInt(val);
}
if (progress === 0) {
var progressText = item.querySelector('.progress_text');
if (progressText) {
var match = progressText.textContent.match(/(\d+)/);
if (match) progress = parseInt(match[1]);
}
}
if (progress === 0) {
var barInner = item.querySelector('.el-progress-bar__inner');
if (barInner) {
var style = barInner.getAttribute('style');
if (style) {
var match = style.match(/width:\s*(\d+)%/);
if (match) progress = parseInt(match[1]);
}
}
}
log('课程[P' + pg + '-' + (idx + 1) + '] ' + name + ' 进度:' + progress + '% 有学习按钮:' + (studyBtn ? '是' : '否'));
if (studyBtn) {
courses.push({ btn: studyBtn, name: name, progress: progress });
}
});
// 过滤未完成的
var todoCourses = courses.filter(function (c) { return c.progress < 100; });
if (todoCourses.length > 0) {
log('第' + pg + '页: ' + todoCourses.length + '门待学习');
var target = todoCourses[0];
setInfo('进入: ' + target.name + ' (' + target.progress + '%)');
setStatus('进入: ' + target.name);
target.btn.click();
await sleep(5000);
if (getPageType() === 'courseList') {
log('还在列表页,重新扫描');
} else {
log('已进入播放页,脚本自动学习中...');
setStatus('播放中...完成后自动回列表');
}
return;
}
log('第' + pg + '页课程全部已完成');
}
setStatus('所有课程已完成');
}
// ==================== 工具 ====================
function waitForEl(selector, timeout) {
return new Promise(function (resolve, reject) {
var el = document.querySelector(selector);
if (el) return resolve(el);
var obs = new MutationObserver(function () {
var el = document.querySelector(selector);
if (el) { obs.disconnect(); resolve(el); }
});
obs.observe(document.body, { childList: true, subtree: true });
setTimeout(function () { obs.disconnect(); reject(new Error('timeout')); }, timeout);
});
}
// ==================== 初始化 ====================
function init() {
var pt = getPageType();
log('页面: ' + pt + ', URL: ' + location.href);
createPanel(pt);
if (pt === 'playPage' && CONFIG.autoStartPlay) {
setTimeout(function () {
if (!isRunning && !shouldStop) startPlayPage();
}, 5000);
}
// 列表页:自动开始扫描(从播放页跳回时会触发)
if (pt === 'courseList') {
setTimeout(function () {
if (!isRunning && !shouldStop) startCourseList();
}, 3000);
}
}
// Vue SPA路由监听
var lastUrl = location.href;
setInterval(function () {
if (location.href !== lastUrl) {
lastUrl = location.href;
log('路由变化: ' + lastUrl);
isRunning = false;
shouldStop = false;
setTimeout(init, 3000);
}
}, 2000);
if (document.readyState === 'complete') init();
else window.addEventListener('load', init);
})();