// ==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 = `

阅读器设置

0 = 不提醒

小猫 or 小狗

计时按墙钟统计;最小化一般仍继续,自动滚动可能变慢;与学习通平台记录可能不一致
`; document.body.appendChild(modal); document.getElementById('mode').onchange = function() { const pixelSetting = document.getElementById('pixelSetting'); pixelSetting.style.display = this.value === 'pixel' ? 'block' : 'none'; }; document.getElementById('save').onclick = () => { CONFIG.scrollSpeed = parseFloat(document.getElementById('speed').value); CONFIG.scrollMode = document.getElementById('mode').value; CONFIG.scrollPixel = parseInt(document.getElementById('pixel').value); CONFIG.autoStart = document.getElementById('autoStart').checked; CONFIG.showTips = document.getElementById('showTips').checked; CONFIG.highlightMode = document.getElementById('highlightMode').checked; CONFIG.loopMode = document.getElementById('loopMode').checked; CONFIG.targetMinutes = Math.max(0, parseInt(document.getElementById('targetMinutes').value, 10) || 0); CONFIG.showChapterTimer = document.getElementById('showChapterTimer').checked; GM_setValue('scrollSpeed', CONFIG.scrollSpeed); GM_setValue('scrollMode', CONFIG.scrollMode); GM_setValue('scrollPixel', CONFIG.scrollPixel); GM_setValue('autoStart', CONFIG.autoStart); GM_setValue('showTips', CONFIG.showTips); GM_setValue('highlightMode', CONFIG.highlightMode); GM_setValue('loopMode', CONFIG.loopMode); GM_setValue('readingTargetMinutes', CONFIG.targetMinutes); GM_setValue('readingShowChapterTimer', CONFIG.showChapterTimer); if (CONFIG.targetMinutes > 0) { GM_setValue(GOAL_NOTIFIED_PREFIX + getReadingTaskKey(), false); } modal.remove(); log('设置已保存'); if (isReadingPage()) { const oldTips = document.getElementById('reading-tips'); if (oldTips) oldTips.remove(); showTips(); } }; document.getElementById('close').onclick = () => { modal.remove(); }; } // 显示快捷键提示 function showTips() { if (!CONFIG.showTips || !isReadingPage()) return; ensureReadingTipsStyle(); const existingTips = document.getElementById('reading-tips'); if (existingTips) existingTips.remove(); const tips = document.createElement('div'); tips.id = 'reading-tips'; tips.style.cssText = ` position: fixed; bottom: 10px; right: 10px; background: rgba(0,0,0,0.7); color: white; padding: 10px; border-radius: 6px; font-size: 12px; z-index: 9998; font-family: Arial, sans-serif; `; document.body.appendChild(tips); updateTips(); } // 键盘事件 function bindKeys() { if (window.readingAssistantKeysbound) return; document.addEventListener('keydown', (e) => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; switch(e.key.toLowerCase()) { case 'k': e.preventDefault(); resumeReading(); break; case 'z': e.preventDefault(); pauseReading(); break; case 's': e.preventDefault(); showSettings(); break; case 'r': e.preventDefault(); resetTaskTimer(); break; } }); window.readingAssistantKeysbound = true; } // 初始化 function init() { bindKeys(); if (isReadingTaskPage()) { autoJumpToReading(); } else if (isReadingPage()) { getReadingTaskKey(); if (CONFIG.scrollMode === 'pixel' && window.readingAssistantInitialized) { showTips(); return; } showTips(); const wasManuallyPaused = GM_getValue('manuallyPaused', false); if (CONFIG.autoStart && !wasManuallyPaused) { setTimeout(startReading, 2000); } else if (wasManuallyPaused) { log('检测到手动暂停状态,不自动开始阅读'); } window.readingAssistantInitialized = true; } } // 启动 init(); })();