// ==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 = '
' + '
' + '🎓 东北财经在线教育学习助手 by齐' + '⏳ 就绪' + '' + '
' + '
' + '
' + '
📊 视频播放进度
' + '
' + '
' + '--:--' + '--:--' + '
' + '
⏰ 预计 --:-- 结束
' + '
' + '
' + '
📋 事件日志
' + '
等待启动...
' + '
' + '
' + '
'; 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 = '' + '' + 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(); } })();