// ==UserScript==
// @name 🫧404小站 — 学习通阅读助手
// @namespace https://scriptcat.org/users/yyy404
// @version 2.0.6
// @description 超星学习通阅读助手:逐段/整页/像素滚动;本任务计时+目标时长达标提醒;K开始/Z暂停/S设置/R重置本任务;同任务内翻章累计、换任务自动隔离
// @author yyy404
// @homepage https://scriptcat.org/zh-CN/script-show-page/3990
// @supportURL https://scriptcat.org/zh-CN/script-show-page/3990/comment
// @icon https://pan-yz.chaoxing.com/favicon.ico
// @icon https://pan-yz.neauce.com/favicon.ico
// @match *://*.chaoxing.com/*
// @match *://mooc1-*.chaoxing.com/*
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// 全局状态管理(跨页面保持)
window.readingAssistantGlobalState = window.readingAssistantGlobalState || {
isRunning: false,
isPaused: false,
scrollPosition: 0,
manuallyPaused: false,
startTime: 0,
chapterElapsed: 0,
timerInterval: null
};
// 防重复跳转锁
let hasAutoJump = false;
// 配置
const CONFIG = {
scrollSpeed: parseFloat(GM_getValue('scrollSpeed', 1.0)),
scrollMode: GM_getValue('scrollMode', 'pixel'), // paragraph, page, pixel
scrollPixel: parseInt(GM_getValue('scrollPixel', 300)),
autoStart: GM_getValue('autoStart', true),
showTips: GM_getValue('showTips', true),
highlightMode: GM_getValue('highlightMode', false),
loopMode: GM_getValue('loopMode', true), // 循环阅读模式
targetMinutes: parseInt(GM_getValue('readingTargetMinutes', 0), 10) || 0,
showChapterTimer: GM_getValue('readingShowChapterTimer', true) !== false
};
const TASK_ELAPSED_PREFIX = 'readingTaskSec_';
const GOAL_NOTIFIED_PREFIX = 'readingGoalDone_';
const SESSION_TASK_KEY = 'readingAssistantTabTaskKey';
let resolvedReadingTaskKey = null;
// 状态
const STATE = {
get isRunning() { return window.readingAssistantGlobalState.isRunning; },
set isRunning(value) { window.readingAssistantGlobalState.isRunning = value; },
get isPaused() { return window.readingAssistantGlobalState.isPaused; },
set isPaused(value) { window.readingAssistantGlobalState.isPaused = value; },
get currentScrollTop() { return window.readingAssistantGlobalState.scrollPosition; },
set currentScrollTop(value) { window.readingAssistantGlobalState.scrollPosition = value; },
get manuallyPaused() { return window.readingAssistantGlobalState.manuallyPaused; },
set manuallyPaused(value) { window.readingAssistantGlobalState.manuallyPaused = value; },
contentElements: [],
currentIndex: 0,
scrollTimer: null
};
// 日志
function log(msg) {
console.log(`[学习通阅读助手] ${msg}`);
}
// 格式化时间(秒 → 00:00:00)
function formatTime(seconds) {
const h = String(Math.floor(seconds / 3600)).padStart(2, '0');
const m = String(Math.floor((seconds % 3600) / 60)).padStart(2, '0');
const s = String(seconds % 60).padStart(2, '0');
return `${h}:${m}:${s}`;
}
// 解析 URL 参数(键名小写)
function parseUrlParams(url) {
try {
const u = new URL(url, location.origin);
const p = {};
u.searchParams.forEach(function (v, k) { p[k.toLowerCase()] = v; });
return p;
} catch (e) {
return {};
}
}
function sanitizeTaskKeyPart(s) {
return String(s || '').replace(/[^\w.-]/g, '_').slice(0, 64);
}
function readTabTaskKeyHint() {
try {
return sessionStorage.getItem(SESSION_TASK_KEY) || '';
} catch (e) {
return '';
}
}
function writeTabTaskKeyHint(key) {
try {
if (key) sessionStorage.setItem(SESSION_TASK_KEY, key);
} catch (e) {}
}
// 阅读任务目录页 → 任务键(含 query,区分同课多任务)
function computeCatalogTaskKey(url) {
url = url || location.href;
let path;
try {
path = new URL(url, location.origin).pathname;
} catch (e) {
path = location.pathname;
}
const m = path.match(/\/course\/(\d+)\.html/i);
const params = parseUrlParams(url);
if (m) {
const qIdx = url.indexOf('?');
const q = qIdx >= 0 ? url.slice(qIdx + 1, qIdx + 81) : '';
return 'rd_c' + m[1] + (q ? '_' + sanitizeTaskKeyPart(q) : '');
}
if (path.indexOf('/zt/portal') !== -1) {
return 'rd_p' + sanitizeTaskKeyPart(params.id || params.courseid || params.jobid || path.replace(/\W/g, '_'));
}
const qIdx = url.indexOf('?');
const qs = qIdx >= 0 ? url.slice(qIdx + 1, qIdx + 81) : 'x';
return 'rd_' + sanitizeTaskKeyPart(path.replace(/\W/g, '_')) + '_' + sanitizeTaskKeyPart(qs);
}
function isReadingChapterFlip() {
const ref = document.referrer || '';
return ref.indexOf('/ztnodedetailcontroller/visitnodedetail') !== -1;
}
// 阅读正文页 → 从 URL 算任务键(仅 cardid/cfid,不含章节级参数)
function computeReadingPageTaskKey(url) {
url = url || location.href;
const p = parseUrlParams(url);
const cid = p.courseid || p.coursid || p.clazzid || '';
const taskPart = p.cardid || p.cfid || '';
if (cid && taskPart) {
return 'rd_' + sanitizeTaskKeyPart(cid) + '_' + sanitizeTaskKeyPart(taskPart);
}
if (taskPart) {
return 'rd_t_' + sanitizeTaskKeyPart(taskPart);
}
return '';
}
function computeReadingPageWeakKey(url) {
url = url || location.href;
const qIdx = url.indexOf('?');
const qs = qIdx >= 0 ? url.slice(qIdx + 1) : '';
if (qs) {
return 'rd_q_' + sanitizeTaskKeyPart(qs.slice(0, 120));
}
try {
return 'rd_path_' + sanitizeTaskKeyPart(new URL(url, location.origin).pathname.replace(/\W/g, '_'));
} catch (e) {
return '';
}
}
function resolveReadingPageTaskKey() {
const tabHint = readTabTaskKeyHint();
// 同任务内翻章:沿用本标签已确定的任务键,避免章节 URL 变化导致计时归零
if (tabHint && isReadingChapterFlip()) {
return tabHint;
}
const fromUrl = computeReadingPageTaskKey(location.href);
if (fromUrl) return fromUrl;
if (tabHint) return tabHint;
const ref = document.referrer || '';
if (ref && (ref.indexOf('/mooc-ans/course/') !== -1 || ref.indexOf('/zt/portal') !== -1)) {
return computeCatalogTaskKey(ref);
}
const weak = computeReadingPageWeakKey(location.href);
if (weak) return weak;
return 'rd_unknown_' + sanitizeTaskKeyPart(location.href.slice(-120));
}
// 当前阅读任务键(按任务隔离;阅读页仅用本页 URL / 本标签 sessionStorage)
function getReadingTaskKey() {
if (resolvedReadingTaskKey) return resolvedReadingTaskKey;
if (isReadingTaskPage()) {
resolvedReadingTaskKey = computeCatalogTaskKey(location.href);
writeTabTaskKeyHint(resolvedReadingTaskKey);
return resolvedReadingTaskKey;
}
if (isReadingPage()) {
resolvedReadingTaskKey = resolveReadingPageTaskKey();
writeTabTaskKeyHint(resolvedReadingTaskKey);
return resolvedReadingTaskKey;
}
resolvedReadingTaskKey = 'rd_unknown';
return resolvedReadingTaskKey;
}
function getTaskSavedElapsed() {
return GM_getValue(TASK_ELAPSED_PREFIX + getReadingTaskKey(), 0);
}
function saveTaskSavedElapsed(seconds) {
GM_setValue(TASK_ELAPSED_PREFIX + getReadingTaskKey(), Math.max(0, Math.floor(seconds)));
}
function getTaskDisplayElapsed() {
return getTaskSavedElapsed() + (window.readingAssistantGlobalState.chapterElapsed || 0);
}
function formatTarget(minutes) {
if (!minutes || minutes <= 0) return '无';
return formatTime(minutes * 60);
}
function getGoalStatus() {
const targetMin = CONFIG.targetMinutes;
if (!targetMin || targetMin <= 0) return 'none';
return getTaskDisplayElapsed() >= targetMin * 60 ? 'done' : 'pending';
}
function renderGoalBadge() {
const status = getGoalStatus();
if (status === 'none') return '';
if (status === 'done') {
return ' ✓ 已达标';
}
const pct = Math.min(100, Math.floor(getTaskDisplayElapsed() / (CONFIG.targetMinutes * 60) * 100));
return ' ✗ 未达标 ' + pct + '%';
}
function ensureReadingTipsStyle() {
if (document.getElementById('reading-tips-style')) return;
const s = document.createElement('style');
s.id = 'reading-tips-style';
s.textContent = [
'.reading-goal-badge{display:inline-block;margin-left:6px;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;vertical-align:middle;}',
'.reading-goal-pending{background:rgba(244,67,54,0.22);color:#ffb4ab;}',
'.reading-goal-ok{background:rgba(76,175,80,0.28);color:#b9f6ca;}',
'.reading-goal-pop{animation:readingGoalPop 0.55s ease;}',
'@keyframes readingGoalPop{0%{transform:scale(1)}45%{transform:scale(1.12)}100%{transform:scale(1)}}'
].join('');
document.head.appendChild(s);
}
function showReadingToast(msg, durationMs) {
durationMs = durationMs || 5000;
let el = document.getElementById('reading-toast');
if (el) el.remove();
el = document.createElement('div');
el.id = 'reading-toast';
el.textContent = msg;
el.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);' +
'background:rgba(40,40,40,0.92);color:#fff;padding:12px 20px;border-radius:8px;' +
'font-size:14px;z-index:10000;max-width:90%;text-align:center;line-height:1.5;';
document.body.appendChild(el);
setTimeout(function () { if (el.parentNode) el.remove(); }, durationMs);
}
function checkReadingGoal() {
if (getGoalStatus() !== 'done') return;
const notifyKey = GOAL_NOTIFIED_PREFIX + getReadingTaskKey();
if (GM_getValue(notifyKey, false)) return;
GM_setValue(notifyKey, true);
log('本任务已达目标时长 ' + CONFIG.targetMinutes + ' 分钟');
const badge = document.getElementById('reading-goal-badge');
if (badge) badge.classList.add('reading-goal-pop');
}
// 本章时长累加到本任务
function flushChapterToTask() {
const ch = window.readingAssistantGlobalState.chapterElapsed || 0;
if (ch > 0) {
saveTaskSavedElapsed(getTaskSavedElapsed() + ch);
window.readingAssistantGlobalState.chapterElapsed = 0;
window.readingAssistantGlobalState.startTime = 0;
}
}
// 启动计时(当前章)
function startTimer() {
if (window.readingAssistantGlobalState.timerInterval) return;
window.readingAssistantGlobalState.startTime = Date.now() - window.readingAssistantGlobalState.chapterElapsed * 1000;
window.readingAssistantGlobalState.timerInterval = setInterval(function () {
window.readingAssistantGlobalState.chapterElapsed = Math.floor((Date.now() - window.readingAssistantGlobalState.startTime) / 1000);
updateTips();
}, 1000);
}
// 停止计时
function stopTimer() {
if (window.readingAssistantGlobalState.timerInterval) {
clearInterval(window.readingAssistantGlobalState.timerInterval);
window.readingAssistantGlobalState.timerInterval = null;
}
}
// 重置当前章时长
function resetChapterTimer() {
stopTimer();
window.readingAssistantGlobalState.chapterElapsed = 0;
window.readingAssistantGlobalState.startTime = 0;
}
// R 键:重置当前阅读任务计时
function resetTaskTimer() {
const taskKey = getReadingTaskKey();
saveTaskSavedElapsed(0);
GM_setValue(GOAL_NOTIFIED_PREFIX + taskKey, false);
resetChapterTimer();
updateTips();
log('本任务阅读时长已重置');
showReadingToast('本任务阅读时长已重置');
}
// 更新提示栏(本任务 + 目标 + 本章)
function updateTips() {
const tips = document.getElementById('reading-tips');
if (!tips) return;
const taskSec = getTaskDisplayElapsed();
const chapter = window.readingAssistantGlobalState.chapterElapsed;
let line2 = '本任务 ' + formatTime(taskSec) + ' / ' + formatTarget(CONFIG.targetMinutes);
if (CONFIG.showChapterTimer) {
line2 += ' · 本章 ' + formatTime(chapter);
}
line2 += renderGoalBadge();
tips.innerHTML = 'K: 开始 | Z: 暂停 | S: 设置 | R: 重置本任务
' + line2;
checkReadingGoal();
}
// 页面检测(兼容新旧目录页)
function isReadingTaskPage() {
return (location.href.includes('/mooc-ans/course/') && location.href.includes('.html')) //感谢用户:ᦸᐝSᴗhi꧂的反馈
|| location.href.includes('/mooc-ans/zt/portal/');
}
function isReadingPage() {
return location.href.includes('/ztnodedetailcontroller/visitnodedetail');
}
// 自动跳转到阅读页面
function autoJumpToReading() {
if (hasAutoJump) return;
if (!isReadingTaskPage()) return;
getReadingTaskKey();
log('检测到阅读任务目录页面,准备跳转');
const observer = new MutationObserver(() => {
const readingLink = document.querySelector(
'a[href*="/ztnodedetailcontroller/visitnodedetail"],.catalog_detail a,.chapter_item a,.nodeItem a,.posCatalog_name a'
); //感谢用户:ᦸᐝSᴗhi꧂的反馈
if (readingLink) {
log('找到阅读章节,正在跳转');
readingLink.click();
hasAutoJump = true;
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
// 收集内容元素
function collectContent() {
const selectors = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img', 'video'];
STATE.contentElements = [];
selectors.forEach(selector => {
document.querySelectorAll(selector).forEach(el => {
if (el.offsetHeight > 20 && el.offsetWidth > 20) {
STATE.contentElements.push(el);
}
});
});
STATE.contentElements.sort((a, b) =>
a.getBoundingClientRect().top - b.getBoundingClientRect().top
);
log(`找到 ${STATE.contentElements.length} 个内容元素`);
}
// 清除高亮
function clearHighlight() {
document.querySelectorAll('.reading-highlight').forEach(el => {
el.classList.remove('reading-highlight');
el.style.outline = '';
});
}
// 段落阅读
function scrollToNext() {
if (STATE.isPaused || STATE.currentIndex >= STATE.contentElements.length) {
completeReading();
return;
}
const element = STATE.contentElements[STATE.currentIndex];
if (CONFIG.highlightMode) {
clearHighlight();
element.classList.add('reading-highlight');
element.style.outline = '4px solid #00FF00';
element.style.transition = 'outline 0.3s ease';
}
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
STATE.currentIndex++;
const randomSpeed = CONFIG.scrollSpeed * (0.9 + Math.random() * 0.3);
STATE.scrollTimer = setTimeout(scrollToNext, randomSpeed * 1000);
}
// 整页阅读
function pageScroll() {
const totalHeight = document.documentElement.scrollHeight - window.innerHeight - 50;
const scrollStep = totalHeight / (CONFIG.scrollSpeed * 10);
if (!STATE.currentScrollTop) {
STATE.currentScrollTop = 0;
}
const scroll = () => {
if (STATE.isPaused) return;
STATE.currentScrollTop += scrollStep;
if (STATE.currentScrollTop >= totalHeight) {
completeReading();
} else {
window.scrollTo({ top: STATE.currentScrollTop, behavior: 'smooth' });
STATE.scrollTimer = setTimeout(scroll, 100);
}
};
scroll();
}
// 像素滚动
function pixelScroll() {
const totalHeight = document.documentElement.scrollHeight - window.innerHeight - 50;
if (!STATE.currentScrollTop) {
STATE.currentScrollTop = window.pageYOffset || document.documentElement.scrollTop;
}
const scroll = () => {
if (STATE.isPaused) return;
STATE.currentScrollTop += CONFIG.scrollPixel;
if (STATE.currentScrollTop >= totalHeight) {
completeReading();
} else {
window.scrollTo({ top: STATE.currentScrollTop, behavior: 'smooth' });
STATE.scrollTimer = setTimeout(scroll, CONFIG.scrollSpeed * 1000);
}
};
scroll();
}
// 完成阅读
function completeReading() {
STATE.isRunning = false;
STATE.isPaused = false;
clearHighlight();
clearTimeout(STATE.scrollTimer);
flushChapterToTask();
resetChapterTimer();
log('阅读完成');
setTimeout(() => {
const nextBtn = document.querySelector('.nodeItem.r i') ||
document.querySelector('a[title="下一章"]') ||
document.querySelector('.next_btn') ||
document.querySelector('.nextBtn') ||
Array.from(document.querySelectorAll('*')).find(el =>
el.textContent && (el.textContent.includes('下一章') || el.textContent.includes('下一节'))
);
if (nextBtn) {
log('找到下一章按钮,正在跳转');
nextBtn.click();
} else if (CONFIG.loopMode) {
log('未找到下一章按钮,循环模式开启,准备跳转到第一章');
setTimeout(() => {
jumpToFirstChapter();
}, 1000);
} else {
log('未找到下一章按钮,阅读结束');
}
}, 2000);
}
// 跳转到第一章
function jumpToFirstChapter() {
log('开始寻找第一章...');
const firstChapterSelectors = [
'.posCatalog_select:first-child a',
'.posCatalog_name:first-child a',
'.catalog_points_yi:first-child a',
'.catalog_title:first-child a',
'.nodeItem:first-child a',
'.catalogDetail:first-child a',
'.catalog_sectionLevel1:first-child a',
'a[href*="/ztnodedetailcontroller/visitnodedetail"]'
];
let firstChapterLink = null;
for (const selector of firstChapterSelectors) {
const links = document.querySelectorAll(selector);
if (links.length > 0) {
firstChapterLink = links[0];
log(`通过选择器 ${selector} 找到第一章链接`);
break;
}
}
if (firstChapterLink) {
log('找到第一章链接,正在跳转...');
firstChapterLink.click();
return;
}
log('尝试返回上级页面...');
if (window.history.length > 1) {
window.history.back();
} else {
const currentUrl = location.href;
const urlParts = currentUrl.split('/');
if (urlParts.length > 3) {
const rootUrl = urlParts.slice(0, 4).join('/');
log(`跳转到根目录: ${rootUrl}`);
location.href = rootUrl;
}
}
}
// 开始阅读
function startReading() {
if (STATE.scrollTimer) clearTimeout(STATE.scrollTimer);
if (STATE.isRunning) return;
startTimer();
STATE.isRunning = true;
STATE.isPaused = false;
STATE.currentIndex = 0;
log(`开始${CONFIG.scrollMode}阅读`);
switch(CONFIG.scrollMode) {
case 'paragraph':
collectContent();
if (STATE.contentElements.length > 0) {
scrollToNext();
} else {
log('未找到段落内容,切换整页模式');
pageScroll();
}
break;
case 'page':
pageScroll();
break;
case 'pixel':
pixelScroll();
break;
}
}
// 暂停阅读
function pauseReading() {
if (STATE.isRunning) {
stopTimer();
STATE.isPaused = true;
STATE.manuallyPaused = true;
GM_setValue('manuallyPaused', true);
clearTimeout(STATE.scrollTimer);
log('阅读已暂停');
}
}
// 继续阅读
function resumeReading() {
if (!STATE.isRunning) {
STATE.manuallyPaused = false;
GM_setValue('manuallyPaused', false);
startTimer();
startReading();
} else {
STATE.isPaused = false;
STATE.manuallyPaused = false;
GM_setValue('manuallyPaused', false);
startTimer();
log('继续阅读');
switch(CONFIG.scrollMode) {
case 'paragraph':
scrollToNext();
break;
case 'page':
pageScroll();
break;
case 'pixel':
pixelScroll();
break;
}
}
}
// 停止阅读
function stopReading() {
STATE.isRunning = false;
STATE.isPaused = false;
STATE.manuallyPaused = false;
clearTimeout(STATE.scrollTimer);
clearHighlight();
flushChapterToTask();
resetChapterTimer();
log('阅读已停止');
}
// 显示阅读器设置面板
function showSettings() {
const existingModal = document.getElementById('reading-settings-modal');
if (existingModal) {
existingModal.remove();
return;
}
const modal = document.createElement('div');
modal.id = 'reading-settings-modal';
modal.style.cssText = `
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: white; padding: 20px; border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 9999;
font-family: Arial, sans-serif; min-width: 320px;
border: 1px solid #ddd;
`;
modal.innerHTML = `
小猫 or 小狗