Greasy Fork is available in English.
自动学习:展开课程、自动播放视频、检测完成并返回;课程跑完后回到“课程班级”并进入下一门未完成课程;记忆已完成课程
// ==UserScript==
// @name 长江雨课堂for_FZU
// @namespace https://github.com/camerayuhang
// @version 1.1.0
// @description 自动学习:展开课程、自动播放视频、检测完成并返回;课程跑完后回到“课程班级”并进入下一门未完成课程;记忆已完成课程
// @author camerayuhang
// @match https://changjiang.yuketang.cn/v2/web/*
// @icon https://www.google.com/s2/favicons?domain=yuketang.cn
// @require https://code.jquery.com/jquery-3.4.1.min.js
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// =======================
// 🌐 自动检测 URL 变化逻辑
// =======================
let lastUrl = location.href;
new MutationObserver(() => {
const currentUrl = location.href;
if (currentUrl !== lastUrl) {
console.log(`🔄 检测到 URL 变化: ${lastUrl} → ${currentUrl}`);
lastUrl = currentUrl;
onUrlChange(currentUrl);
}
}).observe(document, { subtree: true, childList: true });
onUrlChange(location.href);
// ⭐ 新增:全局常量 & 本地存储(课程已完成集合)
const FIN_KEY = 'yt_finished_classes_v1';
const MAX_WAIT_MINUTES = 40;
function loadFinishedSet() {
try {
return new Set(JSON.parse(localStorage.getItem(FIN_KEY) || '[]'));
} catch {
return new Set();
}
}
function saveFinishedSet(set) {
try {
localStorage.setItem(FIN_KEY, JSON.stringify([...set]));
} catch (e) {
console.log('⚠️ 保存已完成课程到本地失败:', e);
}
}
// 根据“课程标题 + 班级名”生成唯一键(两处页面均可获取)
function buildCourseKey({ title, className }) {
return `${(title || '').trim()}|${(className || '').trim()}`.replace(/\s+/g, ' ');
}
function onUrlChange(url) {
if (url.includes('/studentLog/')) {
console.log('📘 当前是课程信息页');
handleStudentLogPage();
} else if (url.includes('/video-student/')) {
console.log('🎬 当前是视频学习页');
handleVideoPage();
} else if (/\/v2\/web\/(index)?$/.test(url) || url.includes('/v2/web/index')) {
console.log('🏠 当前是课程列表页(我听的课)');
handleIndexPage();
} else {
console.log('ℹ️ 当前页面不需要自动化操作');
}
}
// =============== 学习日志页逻辑 ===============
function handleStudentLogPage() {
// 更精确的选择器(按你贴的结构)
const TITLE_SEL = '.headerCard h1 .title-inner-wrapper';
const CLASS_SEL = '.headerCard .classroom-name .title-inner-wrapper';
let booted = false;
const POLL_MS = 400;
const MAX_MS = 15000;
const startAt = Date.now();
const timer = setInterval(() => {
if (Date.now() - startAt > MAX_MS) {
clearInterval(timer);
console.log('⚠️ 等待标题超时,直接尝试任务扫描');
proceed({ title: '', className: '' });
return;
}
const titleEl = document.querySelector(TITLE_SEL);
if (!titleEl) return;
// 读取标题与班级名
const title = (titleEl.textContent || '').trim();
const classNameEl = document.querySelector(CLASS_SEL);
// 兜底:有些页面 classNameEl 可能不存在,尝试取 .classroom-name 整块文本
const className =
(classNameEl && classNameEl.textContent.trim()) ||
(document.querySelector('.headerCard .classroom-name')?.textContent || '').trim() ||
'';
clearInterval(timer);
console.log('✅ 当前课程名称:', title || '(未读取到)');
console.log('✅ 班级名称:', className || '(未读取到)');
proceed({ title, className });
}, POLL_MS);
function proceed(lessonMeta) {
if (booted) return; // 防抖
booted = true;
// ---- 你的原逻辑(基本不动)----
function tryDoTask() {
const expandButtons = Array.from(document.querySelectorAll('span.blue.ml20'))
.filter(el => el.textContent.includes('展开'));
if (expandButtons.length > 0) {
console.log(`✅ 找到 ${expandButtons.length} 个“展开”按钮,正在点击...`);
expandButtons.forEach(btn => btn.click());
console.log('✅ 所有“展开”按钮已点击完成');
setTimeout(findAndClickFirstUnstarted, 3000);
return;
}
console.log('⏳ 未找到“展开”按钮,1秒后重试...');
setTimeout(tryDoTask, 1000);
}
function findAndClickFirstUnstarted() {
const activities = document.querySelectorAll('.activity__wrap');
if (!activities || activities.length === 0) {
console.log('⏳ 未找到任务卡片,1秒后重试...');
setTimeout(findAndClickFirstUnstarted, 1000);
return;
}
let clicked = false;
for (const act of activities) {
const typeUse = act.querySelector('.activity-info .tag use');
if (!typeUse) continue;
const iconHref = typeUse.getAttribute('xlink:href') || '';
if (iconHref !== '#icon-shipin' && iconHref !== '#icon-tuwen') continue;
const aside = act.querySelector('.statistics-box .aside');
if (!aside) continue;
const spans = aside.querySelectorAll('span');
if (spans.length === 0) continue;
const statusText = spans[spans.length - 1].textContent.trim();
if (['已完成', '已读', '未发言'].includes(statusText)) continue;
if (statusText === '未开始' || statusText === '进行中') {
console.log(`🎯 点击第一个 ${statusText} 图文/视频任务:`, act.innerText.trim());
act.scrollIntoView({ behavior: 'smooth', block: 'center' });
const clickable = act.querySelector('.activity-info') || act;
clickable.click();
clicked = true;
break;
}
}
if (!clicked) {
console.log('✅ 没有未开始/进行中的图文或视频 → 视为该课程已完成,准备返回课程列表');
markCurrentCourseFinishedAndBack(lessonMeta);
} else {
console.log('✅ 已点击一个未开始/进行中图文/视频任务');
}
}
function markCurrentCourseFinishedAndBack(meta) {
const finished = loadFinishedSet();
const key = buildCourseKey(meta);
if (key && !finished.has(key)) {
finished.add(key);
saveFinishedSet(finished);
console.log('🧠 已记录完成课程:', key);
} else {
console.log('🧠 完成课程记录已存在或无效key:', key);
}
goBackToCourseList();
}
// 启动实际动作
setTimeout(tryDoTask, 2000);
}
}
// ⭐ 新增:返回“课程班级”(左侧菜单),失败则直接跳转 index
function goBackToCourseList() {
const tryClickMenu = () => {
const lis = Array.from(document.querySelectorAll('.left__menu ul li'));
const target = lis.find(li => (li.textContent || '').includes('课程班级'));
if (target) {
console.log('↩️ 点击左侧菜单 “课程班级”');
target.click();
return true;
}
return false;
};
if (!tryClickMenu()) {
console.log('⚠️ 未找到“课程班级”菜单,直接跳转到 index');
location.href = '/v2/web/index';
}
}
// =============== 视频页逻辑 ===============
function handleVideoPage() {
const CHECK_INTERVAL = 3000;
let elapsedChecks = 0;
// 🔇 进入页面立即尝试静音
function tryMuteVideo() {
let tries = 0;
const MAX_TRIES = 120; // ~1min
const muteInterval = setInterval(() => {
tries++;
const muteIcon = document.querySelector('.xt_video_player_volume .xt_video_player_common_icon');
const video = document.querySelector('video');
if (muteIcon) {
const isMuted = muteIcon.classList.contains('xt_video_player_common_icon_muted');
if (!isMuted) {
console.log('🔇 检测到未静音状态,点击音量图标静音');
muteIcon.click();
} else {
if (video) {
video.muted = true;
video.volume = 0;
}
}
} else if (video) {
video.muted = true;
video.volume = 0;
}
if (tries >= MAX_TRIES) {
console.log('✅ 静音守护结束(持续1分钟)');
clearInterval(muteInterval);
}
}, 500);
}
setTimeout(tryMuteVideo, 1000);
function monitorVideo() {
elapsedChecks++;
const video = document.querySelector('video');
const progressText = document.querySelector('.progress-wrap .text');
if (!video) {
console.log('⏳ 未检测到视频元素,稍后重试...');
setTimeout(monitorVideo, CHECK_INTERVAL);
return;
}
const isPaused = video.paused;
const progress = progressText ? progressText.textContent.trim() : '';
const completed = progress.includes('100%') || document.querySelector('.finish');
console.log(`🎞️ 播放状态: ${isPaused ? '暂停' : '播放中'} | 进度: ${progress}`);
if (isPaused && !completed) {
video.muted = true;
video.play().catch(() => {
console.log('⚠️ 自动播放被阻止,等待用户交互或重试');
});
}
if (completed) {
console.log('✅ 视频已完成播放或检测到已完成标志!2秒后返回课程页...');
setTimeout(goBackToClassPage, 2000);
return;
}
if (elapsedChecks > (MAX_WAIT_MINUTES * 60 * 1000 / CHECK_INTERVAL)) {
console.log('⚠️ 超时未检测到完成状态,强制返回课程页');
goBackToClassPage();
return;
}
setTimeout(monitorVideo, CHECK_INTERVAL);
}
function goBackToClassPage() {
const backBtn = document.querySelector('.header-bar .f14.back');
if (backBtn) {
console.log('↩️ 点击返回按钮');
backBtn.click();
setTimeout(() => {
if (location.href.includes('/studentLog/')) {
console.log('🔄 返回课程页后刷新以确保内容加载');
location.reload();
} else {
console.log('⌛ 等待课程页出现...');
const checkInterval = setInterval(() => {
if (location.href.includes('/studentLog/')) {
clearInterval(checkInterval);
console.log('🔄 检测到课程页,刷新页面');
location.reload();
}
}, 1000);
}
}, 2000);
} else {
console.log('⚠️ 未找到返回按钮,尝试使用浏览器后退');
history.back();
const checkInterval = setInterval(() => {
if (location.href.includes('/studentLog/')) {
clearInterval(checkInterval);
console.log('🔄 检测到课程页(通过后退),刷新页面');
location.reload();
}
}, 1000);
}
}
setTimeout(monitorVideo, 4000);
}
// =============== 课程列表页(我听的课)逻辑 ===============
// ⭐ 新增:自动选择“我听的课”页签;查找下一门未完成课程并点击
function handleIndexPage() {
// 确保切到“我听的课”
function ensureStudentTab(cb) {
const studentTab = document.querySelector('#tab-student');
const active = studentTab && studentTab.classList.contains('is-active');
if (!studentTab) {
console.log('⚠️ 未找到“我听的课”页签,稍后重试');
setTimeout(() => ensureStudentTab(cb), 800);
return;
}
if (!active) {
console.log('🗂️ 切换到“我听的课”页签');
studentTab.click();
setTimeout(cb, 800);
} else {
cb();
}
}
function clickNextUnfinished() {
const finished = loadFinishedSet();
const cards = Array.from(document.querySelectorAll('.TCardGroup .lesson-cardS .el-card__body'))
.map(body => ({
body,
title: (body.querySelector('.left .top h1')?.textContent || '').trim(),
className: (body.querySelector('.left .bottom .className')?.textContent || '').trim()
}))
.filter(x => x.title);
if (!cards.length) {
console.log('⏳ 未找到课程卡片,1秒后重试...');
setTimeout(clickNextUnfinished, 1000);
return;
}
// 依顺序找第一门未记录完成的课
let target = null;
for (const c of cards) {
const key = buildCourseKey(c);
if (!finished.has(key)) {
target = { ...c, key };
break;
}
}
if (!target) {
console.log('🎉 没有新的未完成课程(列表中课程均已标记完成)。');
return;
}
console.log('👉 即将进入下一门未完成课程:', target.title, target.className ? `(${target.className})` : '');
// 点击整卡右/左区都可以触发进入,一般点击 body 的父卡片更稳
const clickableCard = target.body.closest('.el-card') || target.body;
clickableCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => clickableCard.click(), 300);
}
// 尝试点击左侧菜单“课程班级”(如果当前不是该视图)
const inCourseList = !!document.querySelector('.index__view');
if (!inCourseList) {
console.log('ℹ️ 不是标准课程列表主视图,尝试点击左侧“课程班级”');
const ok = (() => {
const lis = Array.from(document.querySelectorAll('.left__menu ul li'));
const target = lis.find(li => (li.textContent || '').includes('课程班级'));
if (target) {
target.click();
return true;
}
return false;
})();
if (!ok) {
location.href = '/v2/web/index';
}
// 待页面切换完再执行
setTimeout(() => ensureStudentTab(clickNextUnfinished), 1000);
} else {
ensureStudentTab(clickNextUnfinished);
}
}
})();