// ==UserScript==
// @name 东财培训自动学习助手
// @namespace http://tampermonkey.net/
// @version 1.3.2
// @description 课程自动播放助手 - 全自动循环学习
// @author 齐
// @match *://trahljkj.edufe.cn/*
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
// =============== 配置 ===============
var CONFIG = {
checkInterval: 3000,
returnDelay: 2000,
startDelay: 3000,
completedThreshold: 100,
popupCheckInterval: 2000,
allCompletedStopCount: 3,
heartbeatInterval: 3000,
pageDetectInterval: 1000,
resumeRetryDelay: 800,
panelDefaultWidth: 400, // 宽度改为 400px
logMinHeight: 100,
logMaxHeightRatio: 0.8,
videoMonitorInterval: 1000,
};
// =============== 状态 ===============
var enabled = true;
var allCompletedCount = 0;
var currentPageType = 'unknown';
var popupRetryCount = 0;
var timers = { check: null, scan: null, popup: null, heartbeat: null, pageDetect: null, videoMonitor: null };
var videoControlInstance = null;
var panel = null;
var isInitialized = false;
var pageObserver = null;
var lastVideoSrc = '';
var lastVideoDuration = 0;
var isReinitializing = false;
var dash = {
chapterCompleted: 0,
chapterTotal: 0,
chapterTitle: '',
lastChapterCompleted: 0,
endedCount: 0,
autoDetectChapters: true,
};
// =============== 工具函数 ===============
function formatTime(date) {
var h = String(date.getHours()).padStart(2, '0');
var m = String(date.getMinutes()).padStart(2, '0');
var s = String(date.getSeconds()).padStart(2, '0');
return h + ':' + m + ':' + s;
}
function pad2(n) { return String(Math.floor(n)).padStart(2, '0'); }
function formatDuration(totalSec) {
if (totalSec <= 0) return '0:00';
var h = Math.floor(totalSec / 3600);
var m = Math.floor((totalSec % 3600) / 60);
var s = Math.floor(totalSec % 60);
if (h > 0) return h + ':' + pad2(m) + ':' + pad2(s);
return m + ':' + pad2(s);
}
// =============== 面板创建(移除章节进度区域,宽度450) ===============
var CSS = ''
+ '#dc-panel{position:fixed;top:10px;left:10px;z-index:999999;'
+ 'width:' + CONFIG.panelDefaultWidth + 'px;background:#fff;border:1px solid #d0d7de;border-radius:12px;'
+ 'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif;'
+ 'font-size:13px;color:#333;box-shadow:0 4px 20px rgba(0,0,0,0.1);user-select:none;min-width:400px;}\n'
+ '#dc-panel *{box-sizing:border-box;margin:0;padding:0;}\n'
+ '#dc-header{cursor:move;padding:12px 16px;display:flex;justify-content:space-between;align-items:center;'
+ 'border-bottom:1px solid #d0d7de;background:#f6f8fa;border-radius:12px 12px 0 0;}\n'
+ '#dc-title{font-size:14px;font-weight:600;color:#24292f;}\n'
+ '#dc-status{padding:2px 10px;border-radius:10px;font-size:11px;font-weight:600;letter-spacing:0.5px;}\n'
+ '#dc-status.playing{background:#dafbe1;color:#1a7f37;}\n'
+ '#dc-status.paused{background:#ffebe9;color:#cf222e;}\n'
+ '#dc-status.stopped{background:#f0f0f0;color:#656d76;}\n'
+ '#dc-body{padding:12px 16px 8px;}\n'
+ '.dc-section{margin-bottom:10px;}\n'
+ '.dc-section-title{font-size:11px;color:#656d76;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;'
+ 'display:flex;align-items:center;gap:6px;}\n'
+ '.dc-section-title::after{content:"";flex:1;height:1px;background:#d0d7de;}\n'
+ '#dc-progress-bar{height:8px;background:#e8eaed;border-radius:4px;overflow:hidden;margin-bottom:8px;}\n'
+ '#dc-progress-fill{height:100%;background:linear-gradient(90deg,#2da44e,#4ac26b);border-radius:4px;'
+ 'transition:width 0.5s ease;width:0%;}\n'
+ '#dc-progress-fill.done{background:linear-gradient(90deg,#1f6feb,#58a6ff);}\n'
+ '#dc-time-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;}\n'
+ '#dc-current-time{font-size:22px;font-weight:700;color:#24292f;font-variant-numeric:tabular-nums;}\n'
+ '#dc-total-time{font-size:22px;font-weight:400;color:#8c959f;font-variant-numeric:tabular-nums;}\n'
+ '#dc-eta{font-size:12px;color:#656d76;text-align:right;}\n'
+ '#dc-eta span{color:#8250df;font-weight:500;}\n'
+ '#dc-eta.done span{color:#1a7f37;}\n'
+ '#dc-log{max-height:500px;overflow-y:auto;font-size:14px;line-height:1.8;color:#333;'
+ 'padding:10px 12px;background:#fff;border:1px solid #d0d7de;border-radius:8px;}\n'
+ '#dc-log .log-row{display:flex;gap:8px;align-items:flex-start;padding:2px 0;}\n'
+ '#dc-log .log-row:hover{background:#f0f6fc;border-radius:3px;}\n'
+ '#dc-log .log-time{color:#57606a;flex-shrink:0;font-size:13px;font-variant-numeric:tabular-nums;white-space:nowrap;}\n'
+ '#dc-log .log-icon{flex-shrink:0;}\n'
+ '#dc-log .log-msg{color:#24292f;word-break:break-all;font-weight:500;}\n'
+ '#dc-log::-webkit-scrollbar{width:7px;}\n'
+ '#dc-log::-webkit-scrollbar-thumb{background:#d0d7de;border-radius:4px;}\n'
+ '#dc-log::-webkit-scrollbar-track{background:#f6f8fa;}\n'
+ '#dc-resize-handle{position:absolute;right:0;top:0;bottom:0;width:8px;cursor:ew-resize;z-index:10;}\n'
+ '#dc-resize-handle:hover{background:rgba(31,111,235,0.1);}\n'
+ '#dc-resize-vertical{height:8px;cursor:ns-resize;background:transparent;border-top:1px solid #d0d7de;'
+ 'border-radius:0 0 12px 12px;transition:background 0.15s;}\n'
+ '#dc-resize-vertical:hover{background:rgba(31,111,235,0.1);}\n'
+ '#dc-toggle-btn{cursor:pointer;padding:2px 8px;border-radius:4px;background:#f0f0f0;'
+ 'font-size:13px;color:#656d76;line-height:1.4;border:none;}\n'
+ '#dc-toggle-btn:hover{background:#ddf4ff;color:#1f6feb;}\n';
function createPanel() {
if (document.getElementById('dc-panel')) return;
GM_addStyle(CSS);
var div = document.createElement('div');
div.id = 'dc-panel';
div.innerHTML =
'
' +
'' +
'' +
'
' +
'
📊 视频播放进度
' +
'
' +
'
' +
'--:--' +
'--:--' +
'
' +
'
⏰ 预计 --:-- 结束
' +
'
' +
'
' +
'
📋 事件日志
' +
'
等待启动...
' +
'
' +
'
' +
'';
document.body.appendChild(div);
// 宽度拖拽
var resizeHandle = div.querySelector('#dc-resize-handle');
var resizing = false, resizeStartX = 0, resizeStartW = 0;
resizeHandle.addEventListener('mousedown', function (e) {
e.preventDefault();
e.stopPropagation();
resizing = true;
resizeStartX = e.clientX;
resizeStartW = div.offsetWidth;
div.style.transition = 'none';
document.body.style.cursor = 'ew-resize';
function onResizeMove(ev) {
if (!resizing) return;
var newW = resizeStartW + (ev.clientX - resizeStartX);
newW = Math.max(400, Math.min(newW, window.innerWidth - 20));
div.style.width = newW + 'px';
}
function onResizeUp() {
resizing = false;
div.style.transition = '';
document.body.style.cursor = '';
document.removeEventListener('mousemove', onResizeMove);
document.removeEventListener('mouseup', onResizeUp);
}
document.addEventListener('mousemove', onResizeMove);
document.addEventListener('mouseup', onResizeUp);
});
// 纵向拖拽
var verticalHandle = div.querySelector('#dc-resize-vertical');
var logEl = div.querySelector('#dc-log');
var isVerticalResizing = false;
var resizeStartY = 0;
var resizeStartHeight = 0;
verticalHandle.addEventListener('mousedown', function (e) {
e.preventDefault();
e.stopPropagation();
isVerticalResizing = true;
resizeStartY = e.clientY;
var currentMaxH = parseInt(window.getComputedStyle(logEl).maxHeight, 10);
if (isNaN(currentMaxH) || currentMaxH <= 0) currentMaxH = 500;
resizeStartHeight = currentMaxH;
document.body.style.cursor = 'ns-resize';
function onResizeMove(ev) {
if (!isVerticalResizing) return;
var deltaY = ev.clientY - resizeStartY;
var newHeight = resizeStartHeight + deltaY;
var minH = CONFIG.logMinHeight;
var maxH = Math.floor(window.innerHeight * CONFIG.logMaxHeightRatio);
newHeight = Math.max(minH, Math.min(maxH, newHeight));
logEl.style.maxHeight = newHeight + 'px';
}
function onResizeUp() {
isVerticalResizing = false;
document.body.style.cursor = '';
document.removeEventListener('mousemove', onResizeMove);
document.removeEventListener('mouseup', onResizeUp);
}
document.addEventListener('mousemove', onResizeMove);
document.addEventListener('mouseup', onResizeUp);
});
// 拖动标题
var header = div.querySelector('#dc-header');
var toggleBtn = div.querySelector('#dc-toggle-btn');
var body = div.querySelector('#dc-body');
var isDragging = false, dragX = 0, dragY = 0;
toggleBtn.addEventListener('click', function (e) {
e.stopPropagation();
var hide = body.style.display !== 'none';
body.style.display = hide ? 'none' : 'block';
toggleBtn.textContent = hide ? '□' : '−';
});
header.addEventListener('mousedown', function (e) {
if (e.target === toggleBtn || e.target === resizeHandle || e.target === verticalHandle) return;
isDragging = true;
var rect = div.getBoundingClientRect();
dragX = e.clientX - rect.left;
dragY = e.clientY - rect.top;
div.style.opacity = '0.85';
function onMove(ev) {
if (!isDragging) return;
var x = ev.clientX - dragX;
var y = ev.clientY - dragY;
div.style.left = Math.max(0, Math.min(x, window.innerWidth - div.offsetWidth)) + 'px';
div.style.top = Math.max(0, Math.min(y, window.innerHeight - 30)) + 'px';
}
function onUp() {
isDragging = false;
div.style.opacity = '1';
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
panel = div;
}
// =============== 仪表盘更新函数 ===============
function setStatus(el, cls, text) {
if (!el) return;
el.className = cls;
el.textContent = text;
}
function trimLog(logEl) {
if (!logEl) return;
var rows = logEl.querySelectorAll('.log-row');
while (rows.length > 30) {
rows[0].parentNode.removeChild(rows[0]);
rows = logEl.querySelectorAll('.log-row');
}
}
function dashLog(icon, msg) {
var logEl = document.querySelector('#dc-log');
if (!logEl) return;
var time = formatTime(new Date());
var row = document.createElement('div');
row.className = 'log-row';
row.innerHTML = '' + time + '' +
'' + icon + '' +
'' + msg + '';
if (logEl.firstChild && logEl.firstChild.tagName === 'SPAN') logEl.innerHTML = '';
logEl.appendChild(row);
trimLog(logEl);
logEl.scrollTop = logEl.scrollHeight;
}
function dashLogNoTime(icon, msg) {
var logEl = document.querySelector('#dc-log');
if (!logEl) return;
var row = document.createElement('div');
row.className = 'log-row';
row.innerHTML = '00:00:00' +
'' + icon + '' +
'' + msg + '';
if (logEl.firstChild && logEl.firstChild.tagName === 'SPAN') logEl.innerHTML = '';
logEl.appendChild(row);
trimLog(logEl);
logEl.scrollTop = logEl.scrollHeight;
}
function updateProgress(currentSec, durationSec, paused) {
var pct = durationSec > 0 ? Math.min(100, Math.round(currentSec / durationSec * 100)) : 0;
var fill = document.querySelector('#dc-progress-fill');
var curEl = document.querySelector('#dc-current-time');
var totEl = document.querySelector('#dc-total-time');
var statusEl = document.querySelector('#dc-status');
var etaEl = document.querySelector('#dc-eta');
var etaSpan = document.querySelector('#dc-eta span');
if (fill) {
fill.style.width = pct + '%';
fill.className = (pct >= 100) ? 'done' : '';
}
if (curEl) curEl.textContent = formatDuration(currentSec);
if (totEl) totEl.textContent = formatDuration(durationSec);
if (pct >= 100) {
setStatus(statusEl, 'stopped', '✅ 已完成');
} else if (paused) {
setStatus(statusEl, 'paused', '⏸ 暂停中');
} else {
setStatus(statusEl, 'playing', '▶ 播放中');
}
if (etaEl && etaSpan) {
if (pct >= 100) {
etaEl.className = 'done';
etaSpan.textContent = '已完成';
} else if (durationSec > 0 && currentSec > 0 && !paused) {
var remainingSec = Math.max(0, durationSec - currentSec);
var etaDate = new Date(Date.now() + remainingSec * 1000);
var etaH = etaDate.getHours();
var etaM = String(etaDate.getMinutes()).padStart(2, '0');
etaSpan.textContent = etaH + ':' + etaM;
etaEl.className = '';
} else if (paused) {
etaSpan.textContent = '已暂停';
etaEl.className = '';
} else {
etaSpan.textContent = '--:--';
etaEl.className = '';
}
}
}
// 章节信息仅通过日志输出,不更新DOM
function updateChapterInfo(completed, total, title, silent) {
if (!silent) {
var prev = dash.lastChapterCompleted;
if (completed > prev && prev > 0) {
dashLog('✅', '第' + prev + '个章节播放完毕');
}
if (completed > prev && prev === 0 && completed > 0) {
dashLog('✅', '第' + completed + '个章节播放完毕');
}
}
dash.lastChapterCompleted = completed;
dash.chapterCompleted = completed;
dash.chapterTotal = total;
dash.chapterTitle = title;
}
// =============== 页面类型识别 ===============
function detectPageType() {
var hasLearnBtn = false;
var btns = document.querySelectorAll('button');
for (var i = 0; i < btns.length; i++) {
if (btns[i].textContent.trim() === '学习') { hasLearnBtn = true; break; }
}
var hasVideo = !!document.querySelector('video');
if (hasLearnBtn) return 'list';
if (hasVideo) return 'play';
return 'unknown';
}
function findChapterTitle() {
var sels = ['h1', 'h2', 'h3', '.title', '[class*="title"]', '[class*="chapter-name"]', '[class*="video-title"]'];
for (var i = 0; i < sels.length; i++) {
var el = document.querySelector(sels[i]);
if (el && el.textContent.trim().length > 2 && el.textContent.trim().length < 200) {
return el.textContent.trim();
}
}
var items = document.querySelectorAll('.active, .current, [class*="active"], [class*="current"], [class*="playing"]');
for (var j = 0; j < items.length; j++) {
var t = items[j].textContent.trim();
if (t.length > 2 && t.length < 200 && !/已完成|已学/.test(t)) return t;
}
return '';
}
function tryCountTotalFromDOM() {
var allDivs = document.querySelectorAll('div');
var bestDiv = null;
var bestCount = 0;
for (var i = 0; i < allDivs.length; i++) {
var d = allDivs[i];
var txt = d.textContent;
var m = txt.match(/(\d+)\s*\/\s*(\d+)/);
if (m && parseInt(m[2]) > parseInt(m[1])) {
var children = d.querySelectorAll('li, [class*="item"], [class*="chapter"]');
if (children.length >= 2 && children.length > bestCount) {
bestDiv = d;
bestCount = children.length;
}
}
}
if (bestCount >= 2) return bestCount;
for (var j = 0; j < allDivs.length; j++) {
var div = allDivs[j];
var items = div.querySelectorAll('li, [class*="item"], [class*="chapter"]');
if (items.length >= 2) {
var doneCount = 0;
for (var k = 0; k < items.length; k++) {
var itemText = items[k].textContent;
if (itemText.indexOf('已完成') !== -1 || itemText.indexOf('已学') !== -1 ||
/completed|done|finish/i.test(items[k].className || '')) {
doneCount++;
}
}
if (doneCount > 0 && items.length > bestCount) {
bestCount = items.length;
}
}
}
return bestCount >= 2 ? bestCount : 0;
}
// =============== 列表页功能 ===============
function findCourseCards() {
var cards = [];
var btns = document.querySelectorAll('button');
btns.forEach(function (btn) {
if (btn.textContent.trim() === '学习') {
var card = btn.closest('div[class*="course"], div[class*="item"], li, div[class*="card"]') || btn.parentElement;
cards.push({ button: btn, card: card });
}
});
return cards;
}
function extractCourseInfo(cardData) {
var card = cardData.card;
var progress = 0, title = '';
var pEl = card.querySelector('[class*="progress"] span, [class*="schedule"] span, [class*="percent"]');
if (pEl) { var m = pEl.textContent.match(/(\d+(?:\.\d+)?)%/); if (m) progress = parseFloat(m[1]); }
if (progress === 0) {
card.querySelectorAll('span').forEach(function (sp) {
if (/^\d+(?:\.\d+)?%$/.test(sp.textContent.trim())) progress = parseFloat(sp.textContent.trim());
});
}
if (progress === 0) {
var el = card.querySelector('[data-progress], [data-schedule]');
if (el) { var v = el.getAttribute('data-progress') || el.getAttribute('data-schedule'); if (v) progress = parseFloat(v); }
}
if (progress === 0) {
var m2 = card.textContent.match(/(\d+(?:\.\d+)?)%/); if (m2) progress = parseFloat(m2[1]);
}
var h3 = card.querySelector('h3, h2, .title, [class*="title"]');
if (h3) title = h3.textContent.trim();
if (!title) {
card.querySelectorAll('div').forEach(function (d) {
var t = d.textContent.trim();
if (t.length > 5 && t.length < 150 && t.indexOf('%') === -1) title = t;
});
}
if (!title) title = card.textContent.substring(0, 60).trim();
return { button: cardData.button, card: card, progress: progress, title: title };
}
function scanAndEnterCourse() {
if (!enabled) return;
var cards = findCourseCards();
if (cards.length === 0) { allCompletedCount = 0; return; }
var courses = cards.map(extractCourseInfo);
var allCompleted = true;
for (var i = 0; i < courses.length; i++) {
if (courses[i].progress < CONFIG.completedThreshold) { allCompleted = false; break; }
}
if (allCompleted) {
allCompletedCount++;
if (allCompletedCount >= CONFIG.allCompletedStopCount) {
dashLog('✅', '所有课程已完成,停止巡检');
enabled = false;
setStatus(document.querySelector('#dc-status'), 'stopped', '✅ 全部完成');
if (timers.scan) { clearInterval(timers.scan); timers.scan = null; }
}
return;
}
allCompletedCount = 0;
for (var j = 0; j < courses.length; j++) {
if (courses[j].progress < CONFIG.completedThreshold) {
dashLog('🎯', '进入课程: ' + courses[j].title + ' (' + courses[j].progress + '%)');
courses[j].button.click();
return;
}
}
}
// =============== 章节完成检测 ===============
function checkChapterCompletion() {
if (!enabled) return;
var completed = 0, total = 0;
var targetList = null;
var lists = document.querySelectorAll('ul, ol, div[class*="chapter-list"], div[class*="section-list"]');
for (var i = 0; i < lists.length; i++) {
var items = lists[i].querySelectorAll('li, div[class*="chapter-item"], div[class*="section-item"]');
if (items.length >= 2) {
var hasStatus = false;
for (var t = 0; t < items.length; t++) {
if (items[t].querySelector('svg, i, [class*="icon"]') ||
items[t].textContent.indexOf('已完成') !== -1 ||
items[t].textContent.indexOf('已学') !== -1) { hasStatus = true; break; }
}
if (hasStatus) { targetList = lists[i]; break; }
}
}
if (!targetList) {
var divs = document.querySelectorAll('div');
for (var k = 0; k < divs.length; k++) {
if (divs[k].textContent.indexOf('已完成') !== -1 && divs[k].children.length > 1) {
targetList = divs[k]; break;
}
}
}
if (targetList) {
var items = targetList.querySelectorAll('li, div[class*="chapter-item"], div[class*="section-item"], div[class*="item"]');
if (items.length >= 1) {
total = items.length;
items.forEach(function (item) {
var greenSvg = item.querySelector('svg path[fill="#52c41a"], svg path[fill="#27ae60"], svg path[fill="#10b981"]');
var hasText = item.textContent.indexOf('已完成') !== -1 || item.textContent.indexOf('已学') !== -1;
var cls = item.className || '';
var hasCls = /completed|done|finish|checked/i.test(cls);
var aria = item.getAttribute('aria-checked') || item.getAttribute('data-status') || item.getAttribute('aria-selected');
var isAttr = (aria === 'true' || aria === 'completed' || aria === 'finished' || aria === 'selected');
var ds = item.getAttribute('data-status');
var isDs = ds && ['finished','completed','done'].indexOf(ds.toLowerCase()) !== -1;
if (greenSvg || hasText || hasCls || isAttr || isDs) completed++;
});
}
}
if (total === 0) {
total = tryCountTotalFromDOM();
}
if (total === 0) {
completed = dash.endedCount;
total = 0;
} else {
if (dash.endedCount > completed) {
completed = dash.endedCount;
if (completed > total) completed = total;
}
}
var chTitle = findChapterTitle();
updateChapterInfo(completed, total, chTitle);
if (total > 0 && completed >= total) {
dashLog('📖', '所有章节已完成,等待评价弹窗...');
if (timers.check) { clearInterval(timers.check); timers.check = null; }
}
}
// =============== 弹窗检测 ===============
function checkAndHandlePopup() {
if (!enabled) return;
var modal = null;
var sels = ['.el-dialog', '.ant-modal', '.modal', '.dialog', 'div[role="dialog"]', 'div[class*="popup"]', 'div[class*="modal"]'];
for (var s = 0; s < sels.length; s++) {
var el = document.querySelector(sels[s]);
if (el && (el.textContent.indexOf('评价') !== -1 || el.textContent.indexOf('评分') !== -1)) { modal = el; break; }
}
if (!modal) {
var allDivs = document.querySelectorAll('div');
for (var d = 0; d < allDivs.length; d++) {
var t = allDivs[d].textContent;
if (t.indexOf('请您对该课程进行评价') !== -1 || t.indexOf('课程评价') !== -1) { modal = allDivs[d]; break; }
}
}
if (!modal) { popupRetryCount = 0; return; }
var btns = modal.querySelectorAll('button');
for (var b = 0; b < btns.length; b++) {
var text = btns[b].textContent.trim();
if (/^(好的|确定|确认|OK|是|我知道了)$/i.test(text)) {
dashLog('💬', '自动关闭评价弹窗 (点击"' + text + '")');
btns[b].click();
dashLogNoTime('🔙', '2秒后返回列表页...');
setTimeout(function () { goBack(); }, CONFIG.returnDelay);
if (timers.popup) { clearInterval(timers.popup); timers.popup = null; }
popupRetryCount = 0;
return;
}
}
if (btns.length > 0) {
dashLog('💬', '尝试点击弹窗按钮...');
btns[0].click();
setTimeout(function () { goBack(); }, CONFIG.returnDelay);
if (timers.popup) { clearInterval(timers.popup); timers.popup = null; }
popupRetryCount = 0;
return;
}
popupRetryCount++;
if (popupRetryCount >= 10) {
dashLog('⚠️', '弹窗按钮不可用,强制返回');
goBack();
if (timers.popup) { clearInterval(timers.popup); timers.popup = null; }
popupRetryCount = 0;
}
}
function goBack() {
var sels = ['a[href*="plan"]', 'a[href*="return"]', 'a[class*="back"]', 'button[class*="back"]', '[class*="back-btn"]'];
for (var i = 0; i < sels.length; i++) {
var el = document.querySelector(sels[i]);
if (el) { el.click(); return; }
}
var links = document.querySelectorAll('nav a, header a, .breadcrumb a');
for (var j = 0; j < links.length; j++) {
if (/返回|计划|列表/i.test(links[j].textContent)) { links[j].click(); return; }
}
if (window.history.length > 1) window.history.back();
}
// =============== 视频控制 ===============
function setupVideoControl() {
var video = document.querySelector('video');
if (!video) { dashLog('⚠️', '未找到视频元素'); return null; }
var destroyed = false;
var userInteracted = false;
var playStartTime = null;
var totalPlayTime = 0;
var lastDashUpdate = 0;
var lastDuration = 0;
var control = {
video: video,
eventListeners: [],
heartbeatTimer: null,
visibilityHandler: null,
init: function () {
dash.chapterCompleted = 0;
dash.chapterTotal = 0;
dash.lastChapterCompleted = 0;
var dur = video.duration || 0;
var cur = video.currentTime || 0;
updateProgress(cur, dur, video.paused);
var chTitle = findChapterTitle();
if (chTitle) dash.chapterTitle = chTitle;
updateChapterInfo(dash.endedCount, 0, dash.chapterTitle, true);
dashLog('🎬', '视频就绪 | 总时长 ' + formatDuration(dur || 0));
var playHandler = function () {
if (destroyed) return;
playStartTime = Date.now();
window.__dcPlayStart = Date.now();
lastDashUpdate = 0;
if (!userInteracted) {
dashLog('▶️', '开始播放' + (video.currentTime > 0 ? ' (从 ' + formatDuration(video.currentTime) + ' 处恢复)' : ''));
}
userInteracted = true;
updateProgress(video.currentTime, video.duration, false);
};
var pauseHandler = function () {
if (destroyed || !enabled) return;
if (video.ended) return;
var now = Date.now();
if (playStartTime) { totalPlayTime += (now - playStartTime) / 1000; playStartTime = null; }
updateProgress(video.currentTime, video.duration, true);
dashLogNoTime('⏸', '视频暂停 (' + formatDuration(video.currentTime) + ' / ' + formatDuration(video.duration) + ')');
setTimeout(function () {
if (!destroyed && enabled && !video.ended && video.paused) control.resumeByClick();
}, CONFIG.resumeRetryDelay);
};
var endedHandler = function () {
if (destroyed) return;
if (playStartTime) { totalPlayTime += (Date.now() - playStartTime) / 1000; playStartTime = null; }
updateProgress(video.duration, video.duration, false);
dash.endedCount++;
dashLog('🏁', '第' + dash.endedCount + '个章节播放完毕 | 时长 ' + formatDuration(totalPlayTime));
totalPlayTime = 0;
if (dash.chapterTotal === 0) {
updateChapterInfo(dash.endedCount, 0, dash.chapterTitle);
}
setTimeout(function () { checkChapterCompletion(); }, 500);
};
var timeupdateHandler = function () {
if (destroyed) return;
var now = Date.now();
if (now - lastDashUpdate > 1500) {
lastDashUpdate = now;
updateProgress(video.currentTime, video.duration, video.paused);
}
if (video.duration > 0 && lastDuration === 0) {
lastDuration = video.duration;
updateProgress(video.currentTime, video.duration, video.paused);
dashLog('🎬', '视频元数据加载完成 | 总时长 ' + formatDuration(video.duration));
}
};
video.addEventListener('play', playHandler);
video.addEventListener('pause', pauseHandler);
video.addEventListener('ended', endedHandler);
video.addEventListener('timeupdate', timeupdateHandler);
control.eventListeners.push({ event: 'play', handler: playHandler });
control.eventListeners.push({ event: 'pause', handler: pauseHandler });
control.eventListeners.push({ event: 'ended', handler: endedHandler });
control.eventListeners.push({ event: 'timeupdate', handler: timeupdateHandler });
var clickHandler = function () { if (!userInteracted) userInteracted = true; };
document.addEventListener('click', clickHandler, true);
control.eventListeners.push({ event: 'click', handler: clickHandler, capture: true, target: document });
control.visibilityHandler = function () {
if (destroyed || !enabled) return;
if (!document.hidden) {
setTimeout(function () {
if (!destroyed && enabled && !video.ended && video.paused && userInteracted)
control.resumeByClick(true);
}, 600);
}
};
document.addEventListener('visibilitychange', control.visibilityHandler);
control.heartbeatTimer = setInterval(function () {
if (destroyed || !enabled) return;
if (video.paused && !video.ended && userInteracted) {
video.muted = true;
control.simulateClick();
video.play().catch(function () {});
}
if (!video.paused) updateProgress(video.currentTime, video.duration, false);
}, CONFIG.heartbeatInterval);
setTimeout(function () {
if (!destroyed && enabled && video.paused && !video.ended) {
video.muted = true;
control.simulateClick();
video.play().catch(function () {});
}
}, 1500);
setStatus(document.querySelector('#dc-status'), 'playing', '▶ 播放中');
},
simulateClick: function () {
try {
var rect = video.getBoundingClientRect();
var cx = rect.left + rect.width / 2;
var cy = rect.top + rect.height / 2;
['mouseenter', 'mouseover', 'mousedown', 'mouseup', 'click'].forEach(function (type) {
video.dispatchEvent(new MouseEvent(type, {
bubbles: true, cancelable: true, view: window,
clientX: cx, clientY: cy, button: 0
}));
});
var touch = new Touch({
identifier: Date.now(), target: video,
clientX: cx, clientY: cy, pageX: cx, pageY: cy,
radiusX: 1, radiusY: 1, rotationAngle: 0, force: 1
});
video.dispatchEvent(new TouchEvent('touchstart', {
bubbles: true, cancelable: true, view: window,
touches: [touch], targetTouches: [touch], changedTouches: [touch]
}));
video.dispatchEvent(new TouchEvent('touchend', {
bubbles: true, cancelable: true, view: window,
touches: [], targetTouches: [], changedTouches: [touch]
}));
} catch (e) {}
},
resumeByClick: function (silent) {
if (destroyed || !enabled) return;
var v = this.video;
if (!v || v.ended || !v.paused) return;
if (document.hidden && !userInteracted) return;
v.muted = true;
this.simulateClick();
var self = this;
v.play().then(function () {
if (!silent) dashLogNoTime('▶️', '恢复播放成功');
if (!playStartTime) playStartTime = Date.now();
updateProgress(v.currentTime, v.duration, false);
}).catch(function (e) {
if (e.name === 'NotAllowedError') {
self.simulateClick();
v.play().catch(function () {});
}
});
},
destroy: function () {
destroyed = true;
if (playStartTime && !video.ended) { totalPlayTime += (Date.now() - playStartTime) / 1000; playStartTime = null; }
if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; }
if (this.visibilityHandler) { document.removeEventListener('visibilitychange', this.visibilityHandler); this.visibilityHandler = null; }
this.eventListeners.forEach(function (item) {
(item.target || video).removeEventListener(item.event, item.handler, item.capture || false);
});
this.eventListeners = [];
dash.chapterCompleted = 0;
dash.chapterTotal = 0;
dash.lastChapterCompleted = 0;
}
};
control.init();
return control;
}
// =============== 页面切换调度 ===============
function onPageTypeChange(newType, force) {
if (!force && newType === currentPageType && isInitialized) return;
clearAllTimers();
currentPageType = newType;
popupRetryCount = 0;
if (!enabled) return;
if (newType === 'list') {
dashLog('📋', '进入列表页,启动课程扫描');
setStatus(document.querySelector('#dc-status'), 'stopped', '📋 列表页');
updateProgress(0, 0, true);
setTimeout(function () {
if (currentPageType === 'list' && enabled) {
scanAndEnterCourse();
timers.scan = setInterval(scanAndEnterCourse, 10000);
}
}, CONFIG.startDelay);
} else if (newType === 'play') {
var video = document.querySelector('video');
if (video) {
lastVideoSrc = video.src || video.getAttribute('src') || '';
lastVideoDuration = video.duration || 0;
}
dashLog('🎬', '进入播放页');
videoControlInstance = setupVideoControl();
timers.check = setInterval(checkChapterCompletion, CONFIG.checkInterval);
timers.popup = setInterval(checkAndHandlePopup, CONFIG.popupCheckInterval);
if (timers.videoMonitor) clearInterval(timers.videoMonitor);
timers.videoMonitor = setInterval(function () {
if (!enabled || currentPageType !== 'play') return;
var v = document.querySelector('video');
if (!v) return;
var currentSrc = v.src || v.getAttribute('src') || '';
var currentDuration = v.duration || 0;
if ((currentSrc && currentSrc !== lastVideoSrc) ||
(currentDuration > 0 && currentDuration !== lastVideoDuration)) {
lastVideoSrc = currentSrc;
lastVideoDuration = currentDuration;
updateProgress(v.currentTime || 0, currentDuration, v.paused);
if (!isReinitializing) {
isReinitializing = true;
dashLog('🔄', '检测到视频源/时长变化,重新初始化...');
if (videoControlInstance) {
videoControlInstance.destroy();
videoControlInstance = null;
}
videoControlInstance = setupVideoControl();
setTimeout(function () { isReinitializing = false; }, 500);
}
}
}, CONFIG.videoMonitorInterval);
setTimeout(function () { checkChapterCompletion(); checkAndHandlePopup(); }, 1500);
}
}
// =============== 页面检测 ===============
function startPageDetection() {
var lastUrl = location.href;
function checkPage() {
var newUrl = location.href;
if (newUrl !== lastUrl) {
lastUrl = newUrl;
onPageTypeChange(detectPageType());
} else {
var type = detectPageType();
if (type !== currentPageType) {
onPageTypeChange(type);
} else if (type === 'play') {
var currentVideo = document.querySelector('video');
if (currentVideo) {
if (videoControlInstance && videoControlInstance.video !== currentVideo) {
onPageTypeChange('play', true);
}
}
}
}
}
window.addEventListener('popstate', checkPage);
window.addEventListener('hashchange', checkPage);
timers.pageDetect = setInterval(checkPage, CONFIG.pageDetectInterval);
pageObserver = new MutationObserver(function () {
if (pageObserver._debounce) clearTimeout(pageObserver._debounce);
pageObserver._debounce = setTimeout(function () {
checkPage();
pageObserver._debounce = null;
}, 300);
});
pageObserver.observe(document.body, { childList: true, subtree: true, attributes: false });
setTimeout(function () {
onPageTypeChange(detectPageType());
isInitialized = true;
}, 1500);
}
function clearAllTimers() {
Object.keys(timers).forEach(function (key) {
if (timers[key]) { clearInterval(timers[key]); clearTimeout(timers[key]); timers[key] = null; }
});
if (videoControlInstance) { videoControlInstance.destroy(); videoControlInstance = null; }
if (pageObserver) { pageObserver.disconnect(); pageObserver = null; }
}
// =============== 菜单 ===============
function registerMenuCommands() {
GM_registerMenuCommand('🔍 扫描未完成课程', function () {
if (currentPageType === 'list') scanAndEnterCourse();
else dashLog('⚠️', '当前不在列表页');
});
GM_registerMenuCommand('🔙 返回列表页', function () { goBack(); });
GM_registerMenuCommand('🔄 重新启用', function () {
if (!enabled) {
enabled = true; allCompletedCount = 0; popupRetryCount = 0;
dashLog('🚀', '已重新启用自动扫描');
onPageTypeChange(detectPageType());
} else { dashLog('📌', '脚本已处于启用状态'); }
});
GM_registerMenuCommand('⏯️ 切换启用', function () {
enabled = !enabled;
if (!enabled) {
clearAllTimers();
setStatus(document.querySelector('#dc-status'), 'stopped', '⏸ 已禁用');
dashLog('⚠️', '脚本已禁用');
} else {
allCompletedCount = 0; popupRetryCount = 0;
dashLog('🚀', '脚本已启用');
onPageTypeChange(detectPageType());
}
});
}
// =============== 初始化 ===============
function init() {
createPanel();
registerMenuCommands();
dashLog('🚀', '东北财经在线教育学习助手 by齐 初始化完成');
startPageDetection();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();