// ==UserScript==
// @name 课程自动翻页导航
// @namespace http://tampermonkey.net/
// @version 1.0.1
// @description 自动滚动页面 + 点击"下一页"按钮,适用于自有网站课程内容管理
// @author You
// @match https://avaryholding.yunxuetang.cn/*
// @grant none
// @run-at document-end
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ==================== 配置区 ====================
const CONFIG = {
// 下一页按钮的选择器(按优先级排列,命中第一个就点击)
nextButtonSelectors: [
// ★ 云学堂 "下一个"按钮
"button:has-text('下一个')",
"button.yxtf-button.is-icon",
".yxtf-button.is-icon",
],
// 每页之间等待时间(毫秒)
waitBetweenPages: 3000,
// 是否自动滚动页面到底部
scrollToBottom: true,
scrollStep: 300,
scrollDelay: 300,
// 最多翻多少页(防无限循环)
maxPages: 50,
// 是否自动开始
autoStart: true,
// 是否显示状态面板
showPanel: true,
// ---- SPA 相关 ----
// 点击后等待按钮消失的最长时间(毫秒)
waitForOldBtnGone: 5000,
// 点击后等待新按钮出现的最大等待时间(毫秒)
waitForNewBtnAppear: 15000,
// 轮询间隔(毫秒)
pollInterval: 500,
// ---- 课程完成检测 ----
// 倒计时选择器(云学堂剩余时间 span)
countdownSelector: 'span.yxt-color-warning',
// 课程标题选择器
courseTitleSelector: '.yxtulcdsdk-course-player_title, h3.course-name, .course-title',
// 剩余时间小于此值(秒)自动跳下一个,设 0 表示不等倒计时
autoAdvanceBelowSeconds: 999999,
// 倒计时归零后等待时间再点下一个(毫秒)
doneWaitMs: 2000,
// 面板刷新间隔(毫秒)
panelRefreshMs: 1000,
};
// ================== 配置区结束 ==================
let pageCount = 0;
let running = false;
let stopFlag = false;
let timerInterval = null;
let waitingForCompletion = false;
let debugCountdownId = null;
let fakeRemainSeconds = null;
let forceNextFlag = false; // debug 计时器归零时,强制触发翻页
let completedCourses = []; // 已完成课程列表
// ---------- 状态面板 ----------
function createPanel() {
if (!CONFIG.showPanel) return;
const panel = document.createElement('div');
panel.id = 'auto-nav-panel';
panel.innerHTML = `
📘 课程:
---
⏱ 剩余时间:
---
📋 课程状态:
检测中...
📊 总进度:
---
📄 页面:
0 / ${CONFIG.maxPages}
🔘 按钮:
检测中...
等待就绪...
⚠ 仅供个人学习测试使用
禁止用于违反平台规则的行为
使用者自行承担全部责任
`;
document.body.appendChild(panel);
document.getElementById('an-btn-start').addEventListener('click', start);
document.getElementById('an-btn-stop').addEventListener('click', stop);
document.getElementById('an-btn-debug').addEventListener('click', toggleDebug);
// ---- 折叠/展开 ----
panel.querySelector('.title').addEventListener('click', function(e) {
panel.classList.toggle('collapsed');
});
// 默认收起
panel.classList.add('collapsed');
// ---- 面板拖拽 ----
makeDraggable(panel);
}
function makeDraggable(el) {
const titleBar = el.querySelector('.title');
if (!titleBar) return;
let startX, startY, startLeft, startTop, dragging = false;
titleBar.addEventListener('mousedown', function(e) {
if (e.target.tagName === 'BUTTON') return; // 不拦截按钮点击
dragging = true;
e.preventDefault();
const rect = el.getBoundingClientRect();
startX = e.clientX;
startY = e.clientY;
startLeft = rect.left;
startTop = rect.top;
el.style.transition = 'none';
// 切换到 left 定位,清除 right
el.style.right = 'auto';
el.style.left = startLeft + 'px';
});
document.addEventListener('mousemove', function(e) {
if (!dragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
el.style.left = Math.max(0, Math.min(startLeft + dx, window.innerWidth - el.offsetWidth)) + 'px';
el.style.top = Math.max(0, Math.min(startTop + dy, window.innerHeight - 40)) + 'px';
});
document.addEventListener('mouseup', function() {
if (dragging) {
dragging = false;
el.style.transition = '';
}
});
}
function updatePanel(nextText, status) {
// page count
const pi = document.getElementById('an-page-info');
if (pi) pi.textContent = pageCount + ' / ' + CONFIG.maxPages;
// button info
const bi = document.getElementById('an-btn-info');
if (bi) bi.textContent = nextText || '---';
// status
const st = document.getElementById('an-status');
if (st) {
st.textContent = status || '';
st.className = 'status';
if (running) st.className += ' running';
}
}
function updateCourseInfo() {
const info = getCourseInfo();
// 课程名
const cn = document.getElementById('an-course-name');
if (cn) {
const prefix = info.isCompleted ? '✅ ' : '';
cn.textContent = prefix + (info.courseName || '(未检测到)');
}
// 当前课程状态
const cs = document.getElementById('an-course-status');
if (cs) {
if (info.isCompleted) {
cs.textContent = '✅ 已完成';
cs.className = 'value done';
} else if (info.remainSeconds !== null && info.remainSeconds > 0) {
cs.textContent = '⏳ 学习中';
cs.className = 'value';
cs.style.color = '#ff9800';
} else {
cs.textContent = '📄 浏览中';
cs.className = 'value';
cs.style.color = '#4fc3f7';
}
}
// 总进度(从页面提取)
const pg = document.getElementById('an-progress');
if (pg) {
pg.textContent = getProgressText();
}
// 剩余时间(始终显示页面真实时间)
const rt = document.getElementById('an-remain-time');
if (rt) {
if (info.remainSeconds === null) {
rt.textContent = '---';
rt.className = 'value countdown';
} else if (info.remainSeconds <= 0) {
rt.textContent = '✅ 已完成';
rt.className = 'value done';
} else {
rt.textContent = formatTime(info.remainSeconds);
rt.className = 'value countdown';
if (info.remainSeconds < 60) rt.className += ' warn';
}
}
// debug 加速倒计时显示(在独立按钮区域)
const di = document.getElementById('an-debug-info');
if (di) {
if (fakeRemainSeconds !== null) {
if (fakeRemainSeconds <= 0) {
di.textContent = '✅ 即将跳转';
di.className = 'value done';
} else {
di.textContent = formatTime(fakeRemainSeconds);
di.className = 'value warn';
}
} else {
di.textContent = '---';
di.className = 'value warn';
}
}
}
function setRunningUI(run) {
const startBtn = document.getElementById('an-btn-start');
const stopBtn = document.getElementById('an-btn-stop');
const debugSection = document.getElementById('an-debug-section');
if (startBtn) startBtn.style.display = run ? 'none' : 'block';
if (stopBtn) stopBtn.style.display = run ? 'block' : 'none';
// debug 区域只在运行后显示,停止时隐藏
if (debugSection) debugSection.style.display = run ? 'block' : 'none';
// 停止时也清掉 debug 状态
if (!run) {
stopDebugCountdown();
const debugBtn = document.getElementById('an-btn-debug');
if (debugBtn) {
debugBtn.textContent = '⚡ 启动加速跳转';
debugBtn.className = 'btn-debug';
}
}
}
// ---------- 查找下一页按钮 ----------
function findNextButton() {
for (const selector of CONFIG.nextButtonSelectors) {
try {
// has-text 是 Playwright 语法,浏览器不支持,需要转换
const el = findElement(selector);
if (el && isVisible(el)) {
return { el, selector };
}
} catch (e) {
// selector parse error, skip
}
}
return null;
}
function findElement(selector) {
// 处理 :has-text('xxx') 伪选择器 → 浏览器原生不支持,手动模拟
const hasTextMatch = selector.match(/:has-text\(['"](.+?)['"]\)/);
if (hasTextMatch) {
const baseSelector = selector.replace(/:has-text\(['"].+?['"]\)/, '').trim();
const text = hasTextMatch[1];
const elements = document.querySelectorAll(baseSelector || '*');
for (const el of elements) {
if (el.textContent && el.textContent.includes(text)) {
return el;
}
}
return null;
}
return document.querySelector(selector);
}
function isVisible(el) {
if (!el) return false;
const style = window.getComputedStyle(el);
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& style.opacity !== '0'
&& el.offsetWidth > 0
&& el.offsetHeight > 0;
}
// ---------- 滚动页面 ----------
async function scrollPage() {
if (!CONFIG.scrollToBottom) return;
return new Promise((resolve) => {
let prevHeight = 0;
let attempts = 0;
const maxAttempts = 50;
function doScroll() {
const currentHeight = document.body.scrollHeight;
if (currentHeight === prevHeight || attempts >= maxAttempts) {
window.scrollTo(0, 0);
resolve();
return;
}
prevHeight = currentHeight;
window.scrollBy(0, CONFIG.scrollStep);
attempts++;
setTimeout(doScroll, CONFIG.scrollDelay);
}
doScroll();
});
}
// ---------- 课程信息提取 ----------
/** 从页面提取课程名和剩余时间 */
function getCourseInfo() {
const info = { courseName: null, remainSeconds: null, isCompleted: false };
// 1. 提取课程名称(云学堂 .yxtf-tooltip 带 ulcdsdk-ellipsis 的 span)
const titleSelectors = [
'span.yxtf-tooltip.ulcdsdk-ellipsis-2',
'span.yxtf-tooltip[class*="ulcdsdk-ellipsis"]',
'.group-title',
'.yxtulcdsdk-course-player_title',
'h3.course-name',
];
for (const sel of titleSelectors) {
const el = document.querySelector(sel);
if (el && el.textContent.trim()) {
info.courseName = el.textContent.trim().slice(0, 60);
break;
}
}
// 2. 检测课程是否已完成(通过 SVG path 图标或父级状态)
info.isCompleted = detectCourseCompleted();
// 2. 提取剩余时间
const countEl = document.querySelector(CONFIG.countdownSelector);
if (countEl) {
const rawText = countEl.textContent.trim();
console.log('[面板] 倒计时原始文本:', JSON.stringify(rawText));
info.remainSeconds = parseCountdownText(rawText);
} else {
// 尝试更宽泛的匹配
const fallbackEl = document.querySelector('.yxt-color-warning');
if (fallbackEl) {
const rawText = fallbackEl.textContent.trim();
console.log('[面板] 倒计时(fallback)原始文本:', JSON.stringify(rawText));
info.remainSeconds = parseCountdownText(rawText);
} else {
console.log('[面板] 未找到倒计时元素');
}
}
return info;
}
/** 检测当前课程是否已完成(通过页面图标/状态判断) */
function detectCourseCompleted() {
// 方式1: 侧边栏当前高亮项带完成图标
// 云学堂已完成项的 SVG path 通常在一个容器内,查找完成标记
const doneIndicators = [
'.yxt-color-success', // 绿色文字/图标 = 已完成
'[class*="success"] svg path', // success class 内的 SVG
'[class*="done"] svg path', // done class 内的 SVG
'[class*="completed"] svg path', // completed class 内的 SVG
'.yxtulcdsdk-item.is-active.is-completed', // 当前激活且已完成
'.yxtulcdsdk-item.active.completed',
'.task-item.completed',
];
for (const sel of doneIndicators) {
const el = document.querySelector(sel);
if (el && isVisible(el)) return true;
}
// 方式2: 倒计时区域没有剩余时间(已经学完)→ 但有完成标记
const countEl = document.querySelector(CONFIG.countdownSelector);
if (!countEl) {
// 无倒计时 → 检查是否有"已完成"/"已学完"文字
const doneTextEls = document.querySelectorAll(
'span, div, .yxtbiz-language-slot, .yxtulcdsdk-course-player_countdown'
);
for (const el of doneTextEls) {
const t = el.textContent || '';
if (t.includes('已完成') || t.includes('已学完') || t.includes('已通过')) {
return true;
}
}
}
// 方式3: 查找侧边栏中当前高亮的课程项,看是否有完成图标
const activeItem = document.querySelector(
'.yxtulcdsdk-item.is-active, .yxtulcdsdk-item.active, [class*="is-active"][class*="task"]'
);
if (activeItem) {
// 在当前激活项内查找完成标记 SVG
const doneSvg = activeItem.querySelector('svg[class*="success"], [class*="done"], [class*="check"]');
if (doneSvg) return true;
// 或者查找绿色 path 元素
const greenPath = activeItem.querySelector('path[fill*="#4caf50"], path[fill*="#52c41a"], path[stroke*="#4caf50"]');
if (greenPath) return true;
}
return false;
}
/** 从页面提取总进度 如 "7%(7/98个任务)" */
function getProgressText() {
const selector = 'span.text-8c.ml12.font-size-12, span.text-8c.ml12, span.text-8c';
const el = document.querySelector(selector);
if (el) {
const t = el.textContent.trim();
const pctMatch = t.match(/(\d+)%/);
if (pctMatch) {
const pct = pctMatch[1];
const fracMatch = t.match(/(\d+)\s*\/\s*(\d+)/);
if (fracMatch) {
return pct + '% (' + fracMatch[1] + '/' + fracMatch[2] + ')';
}
return pct + '%';
}
return t.slice(0, 30);
}
return '---';
}
/** 解析倒计时文字为秒数 */
function parseCountdownText(text) {
if (!text) return null;
// 已经是完成状态 → 返回 0
if (/已完成|已学完|已通过|恭喜|完成/.test(text)) return 0;
// "X小时Y分Z秒" 如 "1小时30分05秒" → 5405
let total = 0;
const hourMatch = text.match(/(\d+)\s*小时/);
const minMatch = text.match(/(\d+)\s*分/);
const secMatch = text.match(/(\d+)\s*秒/);
if (hourMatch) total += parseInt(hourMatch[1]) * 3600;
if (minMatch) total += parseInt(minMatch[1]) * 60;
if (secMatch) total += parseInt(secMatch[1]);
if (total > 0) return total;
// "XX:YY:ZZ" 如 "1:30:05" → 5405
const fullTime = text.match(/(\d+):(\d{2}):(\d{2})/);
if (fullTime) return parseInt(fullTime[1]) * 3600 + parseInt(fullTime[2]) * 60 + parseInt(fullTime[3]);
// "XX:YY" 如 "12:30" → 750
const timeMatch = text.match(/(\d+):(\d{2})/);
if (timeMatch) return parseInt(timeMatch[1]) * 60 + parseInt(timeMatch[2]);
// "XX YY" 空格分隔两个数字,如 "75 33" → 75分33秒 → 4533
const twoNumMatch = text.match(/^(\d+)\s+(\d+)/);
if (twoNumMatch) {
const a = parseInt(twoNumMatch[1]);
const b = parseInt(twoNumMatch[2]);
if (b < 60) return a * 60 + b;
return Math.max(a, b);
}
// 单个纯数字 → 秒数
const numMatch = text.match(/^[\s]*(\d+)[\s]*$/);
if (numMatch) return parseInt(numMatch[1]);
// 第一个数字(兜底)
const firstNum = text.match(/(\d+)/);
if (firstNum) return parseInt(firstNum[1]);
return null;
}
/** 格式化秒数为可读文本,超过1小时才显示小时 */
function formatTime(seconds) {
if (seconds < 60) return seconds + ' 秒';
if (seconds < 3600) {
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return s === 0 ? m + ' 分钟' : m + '分' + s + '秒';
}
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
if (m === 0 && s === 0) return h + ' 小时';
if (s === 0) return h + '小时' + m + '分';
return h + '小时' + m + '分' + s + '秒';
}
// ---------- 实时计时器 ----------
let _timerRunning = false;
function startTimer() {
stopTimer();
updateCourseInfo();
_timerRunning = true;
tickTimer();
}
function tickTimer() {
if (!_timerRunning) return;
updateCourseInfo();
timerInterval = setTimeout(tickTimer, CONFIG.panelRefreshMs);
}
function stopTimer() {
_timerRunning = false;
if (timerInterval) {
clearTimeout(timerInterval);
timerInterval = null;
}
}
// ---------- 课程完成检测 ----------
let _hadCountdown = false; // 记住当前页面曾经有倒计时
function isCourseDone() {
const info = getCourseInfo();
// 倒计时归零
if (info.remainSeconds !== null && info.remainSeconds <= 0) return true;
// 倒计时元素消失 → 说明页面把它移走了(课程完成)
if (_hadCountdown && info.remainSeconds === null) return true;
// 页面有已完成标记
if (info.isCompleted) return true;
return false;
}
/** 检测多设备冲突提示:"系统禁止同时学习多个课程" */
function hasConflictPrompt() {
const el = document.querySelector('div.mt12.color-gray-9');
if (!el || !isVisible(el)) return false;
const t = el.textContent || '';
return t.includes('同时学习') || t.includes('其他课程将暂停');
}
/** 正常等待课程倒计时结束 */
async function waitForCourseCompletion() {
waitingForCompletion = true;
updatePanel('⏳ 等待中', '课程学习中...');
let lastRemain = -1; // 上次余数
let staleCount = 0; // 连续不变次数
const staleMax = 15; // 连续15秒不动→卡死,强跳
let conflictPaused = false;// 是否因多设备冲突而暂停
while (!stopFlag) {
updateCourseInfo();
// 多设备冲突检测 → 暂停等待,不触发卡死
if (hasConflictPrompt()) {
if (!conflictPaused) {
conflictPaused = true;
staleCount = 0; // 重置卡死计数器
console.log('[冲突] 检测到多设备同时学习提示,暂停等待');
}
updatePanel('📱 其他设备学习中', '等待冲突解除...');
await sleep(CONFIG.panelRefreshMs);
continue; // 跳过卡死检测,继续等待
}
// 冲突解除,恢复正常
if (conflictPaused) {
conflictPaused = false;
staleCount = 0;
console.log('[冲突] 提示已消失,恢复正常等待');
}
if (isCourseDone() || forceNextFlag) {
updatePanel('✅ 已完成', '准备跳转...');
await sleep(CONFIG.doneWaitMs);
waitingForCompletion = false;
return true;
}
// 卡死检测:倒计时连续N次不变
const info = getCourseInfo();
if (info.remainSeconds !== null) {
if (info.remainSeconds === lastRemain) {
staleCount++;
if (staleCount >= staleMax) {
console.log('[卡死检测] 倒计时 %d 秒未变化,强制跳转', lastRemain);
updatePanel('⚠ 页面卡死', '强制跳转...');
await sleep(CONFIG.doneWaitMs);
waitingForCompletion = false;
return true;
}
} else {
staleCount = 0;
}
lastRemain = info.remainSeconds;
}
await sleep(CONFIG.panelRefreshMs);
}
waitingForCompletion = false;
return false;
}
// ---------- Debug 独立加速计时器 ----------
function startDebugCountdown() {
stopDebugCountdown();
const info = getCourseInfo();
const original = info.remainSeconds;
if (original === null || original <= 0) {
updatePanel('⚠ 无需加速', '倒计时已完成');
return;
}
const secPart = Math.max(1, original % 60);
forceNextFlag = false;
fakeRemainSeconds = secPart;
updateCourseInfo();
console.log(`[debug] 原${formatTime(original)} → 取秒位 ${secPart}秒`);
const btn = document.getElementById('an-btn-debug');
if (btn) { btn.textContent = '⏹ 取消加速'; btn.className = 'btn-debug running-debug'; }
debugCountdownId = setInterval(() => {
if (stopFlag || !running) {
stopDebugCountdown();
return;
}
fakeRemainSeconds--;
updateCourseInfo();
if (fakeRemainSeconds <= 0) {
// 先设 flag,再清 timer(不清 flag!)
forceNextFlag = true;
fakeRemainSeconds = 0;
updateCourseInfo();
console.log('[debug] ⏰ 加速计时器归零,触发强制翻页');
// 只清 timer 和 UI,保留 forceNextFlag 供主循环检测
if (debugCountdownId) {
clearInterval(debugCountdownId);
debugCountdownId = null;
}
fakeRemainSeconds = null;
const di = document.getElementById('an-debug-info');
if (di) { di.textContent = '✅ 触发跳转'; di.className = 'value done'; }
const b = document.getElementById('an-btn-debug');
if (b) { b.textContent = '⚡ 启动加速跳转'; b.className = 'btn-debug'; }
}
}, 1000);
}
function stopDebugCountdown() {
if (debugCountdownId) {
clearInterval(debugCountdownId);
debugCountdownId = null;
}
fakeRemainSeconds = null;
forceNextFlag = false; // 手动取消时才重置
const di = document.getElementById('an-debug-info');
if (di) { di.textContent = '---'; di.className = 'value warn'; }
const btn = document.getElementById('an-btn-debug');
if (btn) { btn.textContent = '⚡ 启动加速跳转'; btn.className = 'btn-debug'; }
}
/** 切换 debug 加速开/关 */
function toggleDebug() {
if (debugCountdownId) {
// 正在运行 → 取消
stopDebugCountdown();
updatePanel('取消加速', '正常等待中...');
} else {
// 启动
startDebugCountdown();
}
}
/** 轮询等待按钮出现,返回按钮元素,超时返回 null */
async function waitForButton(timeoutMs) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (stopFlag) return null;
const result = findNextButton();
if (result) return result;
await sleep(CONFIG.pollInterval);
}
return null;
}
/** 轮询等待按钮从 DOM 中消失,超时也继续 */
async function waitForButtonGone(oldBtn, timeoutMs) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (stopFlag) return;
if (!document.contains(oldBtn)) return; // 已从 DOM 移除
if (!isVisible(oldBtn)) return; // 已隐藏
await sleep(300);
}
}
// ---------- 主流程 ----------
async function navigateOnePage() {
pageCount++;
stopDebugCountdown(); // 进新页面先清旧加速 timer(手动取消那种)
forceNextFlag = false; // 新页面开始,flag 重置
_hadCountdown = false; // 新页面,重置倒计时标记
updatePanel(`按钮: 等待出现...`, `第 ${pageCount} 节`);
// 1. 等待当前页面的"下一个"按钮出现
const result = await waitForButton(CONFIG.waitForNewBtnAppear);
if (!result) {
updatePanel('⚠ 超时', `第${pageCount}节无按钮`);
console.log('[自动翻页] 等待超时,未找到按钮');
return false;
}
const { el, selector } = result;
updatePanel(`按钮: ${selector}`, `第 ${pageCount} 节`);
// 2. 检测课程是否已完成 → 已完成可直接跳过
const courseInfo = getCourseInfo();
if (courseInfo.isCompleted && courseInfo.courseName && !completedCourses.includes(courseInfo.courseName)) {
completedCourses.push(courseInfo.courseName);
console.log(`[跳过] ${courseInfo.courseName} 已完成`);
// 10 秒倒计时提示
for (let i = 10; i >= 1; i--) {
if (stopFlag) return false;
updatePanel('⏭ 已完成课程', `${i}秒后自动跳过...`);
await sleep(1000);
}
const skipBtn = findNextButton();
if (skipBtn) {
updatePanel('⏭ 跳过中', '该课程已完成');
skipBtn.el.click();
await sleep(CONFIG.waitBetweenPages);
}
return true;
}
// 3. 检查是否有倒计时 → 等待完成(或被加速中断)
if (courseInfo.remainSeconds !== null && courseInfo.remainSeconds > 0) {
_hadCountdown = true; // 标记:当前页面有倒计时
updatePanel(`⏳ 剩余 ${formatTime(courseInfo.remainSeconds)}`, `等待完成...`);
const completed = await waitForCourseCompletion();
if (!completed || stopFlag) return false;
// 课程完成 → 记录
if (courseInfo.courseName && !completedCourses.includes(courseInfo.courseName)) {
completedCourses.push(courseInfo.courseName);
console.log(`[完成] ${courseInfo.courseName} 已累计 ${completedCourses.length} 门`);
}
// 加速触发 → 直接跳转,跳过滚动
if (forceNextFlag) {
console.log('[加速] 倒计时归零,直接跳转');
updatePanel('⚡ 加速跳转', '直接跳转...');
const jumpBtn = findNextButton();
if (jumpBtn) {
jumpBtn.el.click();
await sleep(CONFIG.waitBetweenPages);
}
return true;
}
}
// 3. 正常流程:滚动页面
await scrollPage();
// 记录课程(无倒计时的页面,滚完就算完成)
if (!courseInfo.remainSeconds && courseInfo.courseName && !completedCourses.includes(courseInfo.courseName)) {
completedCourses.push(courseInfo.courseName);
}
// 4. 点击按钮
const finalResult = findNextButton();
if (!finalResult) {
updatePanel('⚠ 按钮消失', `第${pageCount}节`);
return false;
}
updatePanel(`点击: ${finalResult.selector}`, `跳转中...`);
finalResult.el.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(500);
finalResult.el.click();
console.log(`[自动翻页] 第${pageCount}节 → 点击: ${finalResult.selector}`);
// 5. 等待内容切换
await waitForButtonGone(el, CONFIG.waitForOldBtnGone);
await sleep(CONFIG.waitBetweenPages);
return true;
}
async function run() {
if (pageCount >= CONFIG.maxPages) {
console.log(`[自动翻页] 已达最大页数 ${CONFIG.maxPages}`);
stop();
return;
}
if (stopFlag) { stop(); return; }
try {
const hasNext = await navigateOnePage();
if (!hasNext) {
stop();
return;
}
} catch (e) {
console.error('[自动翻页] 出错:', e);
updatePanel('❌ 出错', String(e).slice(0, 30));
stop();
return;
}
if (!stopFlag) {
setTimeout(run, 300);
}
}
// ---------- 控制 ----------
function start() {
if (running) return;
running = true;
stopFlag = false;
pageCount = 0;
completedCourses = [];
setRunningUI(true);
startTimer();
// 自动展开面板
document.getElementById('auto-nav-panel').classList.remove('collapsed');
updatePanel('启动中...', '开始自动翻页');
console.log('[自动翻页] 开始');
run();
}
function stop() {
running = false;
stopFlag = true;
waitingForCompletion = false;
stopDebugCountdown();
stopTimer();
setRunningUI(false);
updatePanel('已停止', `共 ${pageCount} 节`);
updateCourseInfo();
console.log(`[自动翻页] 停止,共处理 ${pageCount} 节`);
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
// ---------- 入口 ----------
function init() {
createPanel();
startTimer(); // 立即开始刷新课程信息
// 异步检测按钮
updatePanel('检测中...', '等待页面就绪');
waitForButton(10000).then(result => {
if (result) {
updatePanel(`✅ 按钮: ${result.selector}`, '可以开始了');
} else {
updatePanel('⚠ 未匹配', '请检查选择器');
}
});
if (CONFIG.autoStart) {
setTimeout(start, 2000);
}
}
// 页面加载完成后初始化
if (document.readyState === 'complete') {
init();
} else {
window.addEventListener('load', init);
}
})();