// ==UserScript== // @name 台州职业技术学院继续教育翻页 // @namespace http://tampermonkey.net/ // @version 29.1 // @description 侦测学完数量变化→自动点击学习→自动点击知道了开始学习→循环(集成倍速控制2x-10x)+ 自动翻页 + 终止检测 // @author You // @match https://zjjxjy.tzvtc.edu.cn/home/online/kjlist.html* // @match https://zjjxjy.tzvtc.edu.cn/home/online/kjlist* // @match https://zjjxjy.tzvtc.edu.cn/* // @grant none // @run-at document-end // ==/UserScript== (function() { 'use strict'; const Log = (...a) => console.log('[AutoStudyV29.1]', ...a); // ==================== 配置 ==================== const CFG = { detectInterval: 120000, // 侦测间隔:120秒(2分钟) clickDelay: 3000, // 点击后等待:3秒 maxRetries: 20, // 最大重试次数 retryDelay: 500, // 重试间隔:0.5秒 autoStart: true, // 页面加载后自动启动 defaultSpeed: 2.0, // 默认倍速 minSpeed: 1.0, // 最小倍速 maxSpeed: 10.0, // 最大倍速 speedStep: 0.5, // 每次调整步长 speedCheckInterval: 1000, // 倍速检测间隔(毫秒) pageSize: 10, // 每页课程数 }; // ==================== 工具函数 ==================== function $(sel) { return document.querySelector(sel); } function $$(sel) { return document.querySelectorAll(sel); } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } function simulateClick(el) { if (!el) return false; try { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); setTimeout(() => { const rect = el.getBoundingClientRect(); const x = rect.left + rect.width/2; const y = rect.top + rect.height/2; ['mouseover','mouseenter','mousedown','mouseup','click'].forEach(t => { el.dispatchEvent(new MouseEvent(t, {bubbles:true, cancelable:true, view:window, clientX:x, clientY:y, button:0})); }); el.click(); }, 300); return true; } catch(e) { Log('❌ 点击失败:', e); return false; } } // ==================== 统计"学完"数量 ==================== function countXuewan() { try { let count = 0; const rows = document.querySelectorAll('tr'); for (let row of rows) { const allElements = row.querySelectorAll('*'); for (let el of allElements) { if (el.children.length === 0) { const text = el.textContent.trim(); if (text === '学完') { count++; break; } } } } if (count === 0) { const allElements = document.querySelectorAll('*'); for (let el of allElements) { if (el.children.length === 0) { const text = el.textContent.trim(); if (text === '学完') { const parent = el.closest('tr, td'); if (parent) { count++; } } } } } return count; } catch(e) { return 0; } } // ==================== 统计本页总课程数 ==================== function countTotalCourses() { try { const rows = document.querySelectorAll('table.layui-table tbody tr'); return rows.length; } catch(e) { return 0; } } // ==================== 页码相关函数 ==================== function getCurrentPage() { try { const activeLi = document.querySelector('.pagination li.active'); if (activeLi) { const span = activeLi.querySelector('span'); if (span) { const num = parseInt(span.textContent.trim()); if (!isNaN(num)) return num; } } return null; } catch(e) { return null; } } function hasNextPage() { try { const currentPage = getCurrentPage(); if (!currentPage) return false; const allPageLinks = document.querySelectorAll('.pagination li a'); for (let link of allPageLinks) { const text = link.textContent.trim(); const num = parseInt(text); if (!isNaN(num) && num > currentPage) { return true; } } const nextBtn = document.querySelector('.pagination li a.next'); if (nextBtn) { const href = nextBtn.getAttribute('href'); if (href && href.indexOf('p/') !== -1) { return true; } } return false; } catch(e) { return false; } } function getNextPageNumber() { try { const currentPage = getCurrentPage(); if (!currentPage) return null; return currentPage + 1; } catch(e) { return null; } } function clickNextPage() { try { const nextPageNum = getNextPageNumber(); if (!nextPageNum) { Log('❌ 无法获取下一页页码'); return false; } Log(`🔍 查找第 ${nextPageNum} 页`); const links = document.querySelectorAll('.pagination li a'); for (let link of links) { const text = link.textContent.trim(); const num = parseInt(text); if (!isNaN(num) && num === nextPageNum) { Log(`✅ 找到第 ${nextPageNum} 页链接,点击翻页`); simulateClick(link); return true; } } const nextBtn = document.querySelector('.pagination li a.next'); if (nextBtn) { Log('✅ 通过 » 按钮翻页'); simulateClick(nextBtn); return true; } Log('❌ 未找到下一页链接'); return false; } catch(e) { Log('❌ 翻页失败:', e); return false; } } // ==================== 查找"学习"按钮 ==================== function findStudyButton() { Log('🔍 查找"学习"按钮...'); const rows = document.querySelectorAll('tr'); for (let row of rows) { if (row.textContent.includes('学完')) { continue; } const clickables = row.querySelectorAll('button, a, span, div, input[type="button"]'); for (let el of clickables) { const text = el.textContent.trim(); if (text === '学习' || text === '开始学习') { if (el.offsetParent !== null && !el.disabled) { Log('✅ 找到学习按钮'); return el; } } } } const allElements = document.querySelectorAll('*'); for (let el of allElements) { const text = el.textContent.trim(); if (text === '学习' || text === '开始学习') { const row = el.closest('tr'); if (row && row.textContent.includes('学完')) { continue; } if (el.offsetParent !== null && !el.disabled) { Log('✅ 找到学习按钮(全局)'); return el; } } } Log('❌ 未找到学习按钮'); return null; } // ==================== 查找"知道了,开始学习"按钮 ==================== function findConfirmButton() { Log('🔍 查找确认按钮...'); const allElements = document.querySelectorAll('button, a, div, span, input'); for (let el of allElements) { const text = el.textContent.trim(); if (text === '知道了,开始学习') { if (el.offsetParent !== null && !el.disabled) { Log('✅ 找到确认按钮'); return el; } } } for (let el of allElements) { const text = el.textContent.trim(); if (text.includes('知道了') && text.includes('开始学习')) { if (el.offsetParent !== null && !el.disabled) { Log('✅ 找到确认按钮(部分匹配)'); return el; } } } const modals = document.querySelectorAll('.modal, .dialog, .layer, .popup, [role="dialog"]'); for (let modal of modals) { if (modal.offsetParent !== null) { const buttons = modal.querySelectorAll('button, a, div, span'); for (let btn of buttons) { const text = btn.textContent.trim(); if (text.includes('知道了') || text.includes('开始学习') || text === '确定') { if (btn.offsetParent !== null && !btn.disabled) { Log('✅ 在弹窗中找到确认按钮'); return btn; } } } } } Log('❌ 未找到确认按钮'); return null; } // ==================== 倍速控制器 ==================== class SpeedController { constructor() { this.currentSpeed = parseFloat(localStorage.getItem('autoStudySpeed')) || CFG.defaultSpeed; this.isRunning = false; this.intervalId = null; this.lastVideo = null; this.speedSetCount = 0; } getSpeed() { return this.currentSpeed; } setSpeed(value) { value = Math.max(CFG.minSpeed, Math.min(CFG.maxSpeed, value)); value = Math.round(value * 10) / 10; this.currentSpeed = value; localStorage.setItem('autoStudySpeed', value.toString()); Log(`🚀 倍速已设置为 ${value}x`); this.updateUI(); this.applyToCurrentVideo(); return value; } increaseSpeed() { return this.setSpeed(this.currentSpeed + CFG.speedStep); } decreaseSpeed() { return this.setSpeed(this.currentSpeed - CFG.speedStep); } quickSetSpeed(value) { return this.setSpeed(value); } applyToCurrentVideo() { const videos = document.querySelectorAll('video'); for (let video of videos) { if (video.playbackRate !== this.currentSpeed) { video.playbackRate = this.currentSpeed; Log(`✅ 视频倍速已应用: ${this.currentSpeed}x`); video.addEventListener('ratechange', () => { if (video.playbackRate !== this.currentSpeed && video.playbackRate !== 0) { video.playbackRate = this.currentSpeed; Log(`🔄 视频倍速被重置,重新设置为 ${this.currentSpeed}x`); } }); video.addEventListener('loadstart', () => { setTimeout(() => { if (video.playbackRate !== this.currentSpeed) { video.playbackRate = this.currentSpeed; Log(`🔄 视频重新加载,设置倍速为 ${this.currentSpeed}x`); } }, 500); }); } } } checkAndSetSpeed() { if (!this.isRunning) return; const videos = document.querySelectorAll('video'); if (videos.length === 0) { this.lastVideo = null; return; } for (let video of videos) { if (video !== this.lastVideo || video.playbackRate !== this.currentSpeed) { if (video.playbackRate !== this.currentSpeed) { video.playbackRate = this.currentSpeed; this.speedSetCount++; Log(`🎬 设置视频倍速: ${this.currentSpeed}x (第${this.speedSetCount}次)`); video.addEventListener('ratechange', () => { if (video.playbackRate !== this.currentSpeed && video.playbackRate !== 0) { video.playbackRate = this.currentSpeed; } }); } this.lastVideo = video; } } } start() { if (this.isRunning) { Log('⚠️ 倍速控制器已在运行'); return; } this.isRunning = true; this.speedSetCount = 0; this.lastVideo = null; Log(`🚀 倍速控制器启动,当前倍速: ${this.currentSpeed}x`); this.updateUI(); setTimeout(() => this.checkAndSetSpeed(), 500); this.intervalId = setInterval(() => { this.checkAndSetSpeed(); }, CFG.speedCheckInterval); } stop() { this.isRunning = false; if (this.intervalId) { clearInterval(this.intervalId); this.intervalId = null; } this.lastVideo = null; Log('⏹️ 倍速控制器已停止'); this.updateUI(); } updateUI() { const speedDisplay = document.querySelector('#v29-speed-display'); if (speedDisplay) { speedDisplay.textContent = this.currentSpeed.toFixed(1) + 'x'; } const speedBtns = document.querySelectorAll('.v29-speed-btn'); for (let btn of speedBtns) { const val = parseFloat(btn.dataset.speed); if (val === this.currentSpeed) { btn.style.background = '#ffd93d'; btn.style.color = '#333'; } else { btn.style.background = 'rgba(255,255,255,0.15)'; btn.style.color = 'white'; } } } getSpeedText() { return this.currentSpeed.toFixed(1) + 'x'; } } // ==================== 核心学习类 ==================== class AutoStudy { constructor() { this.running = false; this.isBusy = false; this.lastCount = -1; this.totalLearning = 0; this.timer = null; this.hasStartedLearning = false; this.speedController = new SpeedController(); this.isSpeedPanelOpen = false; this.noChangeCount = 0; this.isFinished = false; this.initialized = false; // 新增:是否已完成初始化 } async doStudyFlow() { if (this.isBusy) { Log('⏳ 流程正在执行...'); return; } if (this.isFinished) { Log('✅ 所有课程已完成,停止学习'); return; } this.isBusy = true; Log('═══════════════════════════════════'); Log('🚀 开始学习流程'); Log('═══════════════════════════════════'); try { await sleep(500); // 先检查是否有学习按钮 let studyBtn = findStudyButton(); if (!studyBtn) { Log('📄 没有学习按钮'); // 检查是否所有课程都学完了 const currentCount = countXuewan(); const totalCourses = countTotalCourses(); if (currentCount === totalCourses && totalCourses > 0) { // 本页所有课程已学完 Log(`📊 本页全部学完 (${currentCount}/${totalCourses})`); if (hasNextPage()) { Log('🔄 尝试翻页到下一页'); if (clickNextPage()) { Log('📄 翻页成功,等待页面加载'); this.lastCount = -1; this.noChangeCount = 0; this.initialized = false; this.isBusy = false; // 等待页面刷新后重新初始化 setTimeout(() => { this.detectLoop(); }, 5000); return; } } else { Log('🎉 所有课程已完成!'); this.isFinished = true; this.stop(); this.updateStatus('🎉 全部完成!'); this.isBusy = false; return; } } // 没有学习按钮且未学完,可能页面还没加载好 Log('⏳ 等待页面加载...'); this.isBusy = false; return; } // 找到学习按钮,点击学习 studyBtn.style.border = '3px solid #ff6b6b'; studyBtn.style.boxShadow = '0 0 20px rgba(255,107,107,0.5)'; Log('🖱️ 点击【学习】按钮'); this.updateStatus('点击学习...'); simulateClick(studyBtn); this.hasStartedLearning = true; await sleep(CFG.clickDelay); Log('🔍 查找确认按钮...'); this.updateStatus('等待确认按钮...'); let confirmBtn = null; let retryCount = 0; while (!confirmBtn && retryCount < CFG.maxRetries) { confirmBtn = findConfirmButton(); if (!confirmBtn) { retryCount++; await sleep(CFG.retryDelay); } } if (confirmBtn) { confirmBtn.style.border = '3px solid #ff0000'; confirmBtn.style.boxShadow = '0 0 20px rgba(255,0,0,0.5)'; Log('🖱️ 点击【知道了,开始学习】'); this.updateStatus('开始学习...'); simulateClick(confirmBtn); this.totalLearning++; Log(`✅ 正学: ${this.totalLearning}`); this.updateStatus(`✅ 正学 ${this.totalLearning} 个 | 等待完成...`); this.updateStats(); // 重置无变化计数,因为开始了新的学习 this.noChangeCount = 0; await sleep(2000); } else { Log('❌ 未找到确认按钮'); this.updateStatus('⚠️ 未找到确认按钮'); } } catch(e) { Log('❌ 错误:', e); this.updateStatus('❌ 发生错误'); } finally { this.isBusy = false; Log('🏁 学习流程结束'); } } async detectLoop() { if (!this.running || this.isBusy) return; try { const currentCount = countXuewan(); const totalCourses = countTotalCourses(); Log(`📊 侦测: 学完=${currentCount}/${totalCourses}, 上次记录=${this.lastCount}`); // ===== 初始化:第一次运行 ===== if (this.lastCount === -1) { this.lastCount = currentCount; this.noChangeCount = 0; this.initialized = true; Log(`📝 初始化完成: 学完=${currentCount}`); this.updateStatus(`📝 初始化完成,学完: ${currentCount}`); this.updateStats(); // 如果有未学完的课程,开始学习 if (currentCount < totalCourses) { Log('🎯 开始学习第一个课程...'); setTimeout(() => this.doStudyFlow(), 1500); } else if (currentCount === totalCourses && totalCourses > 0) { // 本页全部学完 Log('📊 本页全部学完,检查翻页'); if (hasNextPage()) { Log('🔄 翻页到下一页'); if (clickNextPage()) { Log('📄 翻页成功'); this.lastCount = -1; this.noChangeCount = 0; this.initialized = false; setTimeout(() => this.detectLoop(), 5000); return; } } else { Log('🎉 所有课程已完成!'); this.isFinished = true; this.stop(); this.updateStatus('🎉 全部完成!'); return; } } return; } // ===== 翻页触发:学完 == 10 ===== if (currentCount === CFG.pageSize && currentCount > this.lastCount) { Log(`🎯 学完达到 ${CFG.pageSize},准备翻页`); this.lastCount = currentCount; this.updateStats(); if (clickNextPage()) { Log('📄 翻页成功,等待页面加载'); this.lastCount = -1; this.noChangeCount = 0; this.initialized = false; this.updateStatus(`📄 翻页到第 ${getCurrentPage() || '?'} 页`); // 等待页面刷新 setTimeout(() => this.detectLoop(), 5000); return; } else { Log('❌ 翻页失败,检查是否最后一页'); if (!hasNextPage()) { Log('🎉 所有课程已完成!停止运行'); this.isFinished = true; this.stop(); this.updateStatus('🎉 全部完成!'); return; } } } // ===== 学完数量增加:继续学习 ===== if (currentCount > this.lastCount) { const increase = currentCount - this.lastCount; Log(`🎯 学完+${increase} (${this.lastCount}→${currentCount})`); this.lastCount = currentCount; this.noChangeCount = 0; this.updateStats(); this.updateStatus(`🎯 学完+${increase},继续学习`); // 如果还没学完,继续学习 if (currentCount < totalCourses) { setTimeout(() => this.doStudyFlow(), 1000); } else if (currentCount === totalCourses && totalCourses > 0) { // 本页全部学完,检查翻页 if (hasNextPage()) { Log('🔄 本页学完,自动翻页'); if (clickNextPage()) { Log('📄 翻页成功'); this.lastCount = -1; this.noChangeCount = 0; this.initialized = false; setTimeout(() => this.detectLoop(), 5000); return; } } else { Log('🎉 所有课程已完成!'); this.isFinished = true; this.stop(); this.updateStatus('🎉 全部完成!'); return; } } return; } // ===== 终止检测:连续2次学完数量没变化,且没有下一页 ===== if (currentCount === this.lastCount && currentCount > 0) { this.noChangeCount++; Log(`⏳ 学完数量未变化 (${this.noChangeCount}/2),当前: ${currentCount}`); if (this.noChangeCount >= 2) { // 检查本页是否全部学完 if (currentCount === totalCourses && totalCourses > 0) { Log('📊 本页全部学完但未触发翻页,尝试手动翻页'); if (hasNextPage()) { if (clickNextPage()) { Log('📄 手动翻页成功'); this.lastCount = -1; this.noChangeCount = 0; this.initialized = false; setTimeout(() => this.detectLoop(), 5000); return; } } else { Log('🎉 所有课程已完成!停止运行'); this.isFinished = true; this.stop(); this.updateStatus('🎉 全部完成!'); return; } } else if (!hasNextPage()) { Log('🎉 连续2次检测无变化,且没有下一页,所有课程已完成!'); this.isFinished = true; this.stop(); this.updateStatus('🎉 全部完成!'); return; } else { Log('⚠️ 有下一页但学完数量未变化,尝试强制学习'); // 尝试点击学习 setTimeout(() => this.doStudyFlow(), 1000); // 重置计数器,避免频繁触发 this.noChangeCount = 0; } } } else { // 有变化,重置计数器 if (currentCount !== this.lastCount) { this.noChangeCount = 0; this.lastCount = currentCount; Log(`📝 更新记录: 学完=${currentCount}`); } } // ===== 正常状态显示 ===== const waitMinutes = Math.round(CFG.detectInterval / 60000); const pageInfo = getCurrentPage() ? `第${getCurrentPage()}页` : ''; this.updateStatus(`⏳ 等待中 | 学完:${currentCount}/${totalCourses} | ${pageInfo} | 间隔:${waitMinutes}分钟`); this.updateStats(); } catch(e) { Log('❌ 侦测错误:', e); } } start() { if (this.running) { Log('⚠️ 已在运行'); return; } this.running = true; this.isBusy = false; this.lastCount = -1; this.totalLearning = 0; this.hasStartedLearning = false; this.noChangeCount = 0; this.isFinished = false; this.initialized = false; Log('═══════════════════════════════════'); Log('🚀 V29.1 启动'); Log(`⏱️ 侦测间隔: ${CFG.detectInterval/60000} 分钟`); Log(`🚀 倍速: ${this.speedController.getSpeedText()}`); Log(`📄 当前页码: ${getCurrentPage() || '未知'}`); Log('═══════════════════════════════════'); this.updateStatus('🚀 启动中...'); this.speedController.start(); if (this.timer) clearInterval(this.timer); this.timer = setInterval(() => { this.detectLoop().catch(e => Log('❌ 错误:', e)); }, CFG.detectInterval); // 延迟启动,确保页面完全加载 setTimeout(() => this.detectLoop(), 2000); } stop() { this.running = false; this.isBusy = false; this.speedController.stop(); if (this.timer) { clearInterval(this.timer); this.timer = null; } Log('⏹️ 已停止'); this.updateStatus('⏹️ 已停止'); const startBtn = document.querySelector('#v29-start'); if (startBtn) { startBtn.textContent = '▶ 开始'; startBtn.style.background = '#00d2ff'; } } updateStatus(text) { const el = document.querySelector('#v29-status'); if (el) el.textContent = text; } updateStats() { const count = countXuewan(); const total = countTotalCourses(); const countEl = document.querySelector('#v29-count'); if (countEl) countEl.textContent = `${count}/${total}`; const learningEl = document.querySelector('#v29-learning'); if (learningEl) learningEl.textContent = this.totalLearning; const progressEl = document.querySelector('#v29-progress'); if (progressEl) { const totalAll = 50; const progress = Math.min(100, Math.round((this.totalLearning / totalAll) * 100)); progressEl.textContent = progress + '%'; } const pageEl = document.querySelector('#v29-page'); if (pageEl) { const page = getCurrentPage(); pageEl.textContent = page || '?'; } const noChangeEl = document.querySelector('#v29-nochange'); if (noChangeEl) { noChangeEl.textContent = `${this.noChangeCount}/2`; } } refreshStats() { const count = countXuewan(); this.lastCount = count; this.noChangeCount = 0; this.updateStats(); this.updateStatus(`已刷新 | 学完:${count}`); Log(`🔄 刷新: 学完=${count}`); return count; } toggleSpeedPanel() { this.isSpeedPanelOpen = !this.isSpeedPanelOpen; const panel = document.querySelector('#v29-speed-panel'); const toggle = document.querySelector('#v29-speed-toggle'); if (panel) { panel.style.display = this.isSpeedPanelOpen ? 'block' : 'none'; } if (toggle) { toggle.textContent = this.isSpeedPanelOpen ? '▲ 收起倍速设置' : '▼ 展开倍速设置'; } } updateSpeedUI() { this.speedController.updateUI(); const display = document.querySelector('#v29-speed-display'); if (display) { display.textContent = this.speedController.getSpeedText(); } } } // ==================== UI 界面 ==================== function createUI(study) { if (window.self !== window.top) return; const oldUI = document.querySelector('#auto-study-v29'); if (oldUI) oldUI.remove(); const div = document.createElement('div'); div.id = 'auto-study-v29'; div.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 2147483647; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 16px; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.4); font-family: "Microsoft YaHei", sans-serif; min-width: 280px; font-size: 13px; border: 2px solid rgba(255,255,255,0.2); user-select: none; max-width: 300px; `; const savedSpeed = parseFloat(localStorage.getItem('autoStudySpeed')) || CFG.defaultSpeed; div.innerHTML = `