// ==UserScript== // @name 安徽继续教育小助手 // @namespace http://tampermonkey.net/ // @version 1.3 // @description 修复作业页跳转失败 | 修复浏览器缩小后视频暂停 | 视频页精准识别 | 自动播放保活 // @author 1678955137 // @match *://main.ahjxjy.cn/study/html/content/studying/* // @match *://main.ahjxjy.cn/study/html/content/process/* // @match *://main.ahjxjy.cn/studentstudio/course/studying* // @grant GM_addStyle // @grant GM_notification // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @grant unsafeWindow // @run-at document-end // @license GPL 3 // ==/UserScript== (function() { 'use strict'; const unsafe = unsafeWindow || window; const state = { panel: null, isCollapsed: GM_getValue('panelCollapsed', false), currentSpeed: GM_getValue('playbackSpeed', 1.5), volumeLevel: GM_getValue('volumeLevel', 0), isVideoAutoJumpEnabled: GM_getValue('isVideoAutoJumpEnabled', true), isHomeworkAutoJumpEnabled: GM_getValue('isHomeworkAutoJumpEnabled', true), autoNextCourse: GM_getValue('autoNextCourse', false), logMessages: [], pos: GM_getValue('panelPos', {left: '20px', top: '20%'}), hasProcessedTextPage: false, homeworkProcessed: false, homeworkLogged: false, endJumpScheduled: false, pageType: 'unknown', lastUrl: location.href, textConfirmTimer: null, textJumpTimer: null, lastVideoCheck: 0 }; // --- 样式定义 --- GM_addStyle(` :root { --primary: #4CAF50; --bg: rgba(255, 255, 255, 0.95); } #gemini-panel { position: fixed; z-index: 2147483647; width: 300px; background: var(--bg); backdrop-filter: blur(12px); border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); border: 1px solid rgba(0,0,0,0.05); transition: width 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28), height 0.3s, opacity 0.3s; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } #gemini-panel.collapsed { width: 50px; height: 50px; border-radius: 25px; cursor: pointer; overflow: hidden; } .panel-header { padding: 12px; background: var(--primary); color: white; border-top-left-radius: 12px; border-top-right-radius: 12px; cursor: move; display: flex; justify-content: space-between; align-items: center; font-weight: bold; } .panel-body { padding: 15px; display: flex; flex-direction: column; gap: 15px; } .collapsed .panel-body, .collapsed .panel-header span { display: none; } .collapsed .panel-header { height: 100%; padding: 0; justify-content: center; background: var(--primary); border-radius: 25px; } .control-row { display: flex; align-items: center; justify-content: space-between; font-size: 14px; color: #333; } .slider-ui { flex: 1; margin: 0 10px; height: 4px; accent-color: var(--primary); cursor: pointer; } .btn-action { width: 100%; padding: 10px; border: none; border-radius: 8px; background: var(--primary); color: white; font-weight: bold; cursor: pointer; transition: transform 0.1s; } .btn-action:active { transform: scale(0.96); } .btn-next { background: #2196F3 !important; } .btn-find-unstudied { background: #FF9800 !important; } .btn-qq { background: #9b59b6 !important; } .log-area { height: 120px; background: #1e1e1e; color: #adff2f; padding: 8px; font-size: 12px; border-radius: 6px; overflow-y: auto; white-space: pre-wrap; box-shadow: inset 0 2px 5px rgba(0,0,0,0.2); line-height: 1.4; } .switch { position: relative; display: inline-block; width: 40px; height: 20px; } .switch input { opacity: 0; width: 0; height: 0; } .slider-round { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 20px; } .slider-round:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .slider-round { background-color: var(--primary); } input:checked + .slider-round:before { transform: translateX(20px); } `); // --- 日志输出 --- function log(msg) { const time = new Date().toLocaleTimeString([], { hour12: false }); state.logMessages.push(`[${time}] ${msg}`); if (state.logMessages.length > 30) state.logMessages.shift(); const el = document.getElementById('logArea'); if (el) { el.textContent = state.logMessages.join('\n'); el.scrollTop = el.scrollHeight; } } // --- API 获取目录并跳转下一节--- function post(url, data) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ url: url, method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: Object.keys(data).map(k => k + '=' + encodeURIComponent(data[k])).join('&'), onload: r => { try { resolve(JSON.parse(r.responseText)); } catch (e) { reject(e); } }, onerror: reject }); }); } function getQuery(name) { return new URLSearchParams(location.search).get(name) || ''; } function getDesignCells(courseOpenId) { return post(location.origin + '/study/design/design', { courseOpenId: courseOpenId, schoolCode: getQuery('schoolCode') || '005' }).then(function (r) { if (r.code !== 1) return []; var cells = []; (r.list || []).forEach(function (m) { (m.lessons || []).forEach(function (l) { (l.cells || []).forEach(function (c) { cells.push({ id: c.id, title: c.title || '', status: !!c.status }); }); }); }); return cells; }).catch(function () { return []; }); } function unitUrl(courseOpenId, cellId) { return location.origin + '/study/html/content/studying/?courseOpenId=' + encodeURIComponent(courseOpenId) + '&cellId=' + encodeURIComponent(cellId) + '&schoolCode=' + (getQuery('schoolCode') || '005'); } function apiJumpNext() { var coid = getQuery('courseOpenId'); if (!coid) { jumpNextManually(); return; } var currentCell = getQuery('cellId'); getDesignCells(coid).then(function (cells) { if (cells.length === 0) { jumpNextManually(); return; } var idx = -1; cells.forEach(function (c, i) { if (c.id === currentCell) idx = i; }); var next = null; if (idx >= 0 && idx < cells.length - 1) { next = cells[idx + 1]; } if (next) { log('➡ 跳转下一单元:' + (next.title || next.id)); location.href = unitUrl(coid, next.id); } else { log('🏁 本课程全部完成,回到课程列表'); location.href = location.origin + '/studentstudio/course/studying?schoolCode=' + (getQuery('schoolCode') || '005'); } }); } // --- 自动展开所有折叠目录 --- function expandAllFolder() { const toggleBtns = document.querySelectorAll('.toggle_lesson'); toggleBtns.forEach(btn => { const childUl = btn.nextElementSibling?.nextElementSibling; if (childUl) { const isHide = childUl.classList.contains('hidden') || getComputedStyle(childUl).display === 'none'; if (isHide) { btn.click(); log("自动展开折叠任务菜单"); } } }); } // --- 查找视频元素(主文档 + 同源 iframe)--- function getVideoEl() { try { const v = document.querySelector('video, .jw-video'); if (v) return v; } catch (e) {} try { const iframes = document.querySelectorAll('iframe'); for (const f of iframes) { try { const doc = f.contentDocument || (f.contentWindow && f.contentWindow.document); if (!doc) continue; const v2 = doc.querySelector('video, .jw-video'); if (v2) return v2; } catch (e2) {} } } catch (e) {} return null; } // --- 判断页面是否存在播放器 --- function hasVideoPlayer() { if (getVideoEl()) return true; try { if (document.querySelector('.jwplayer, .jw-media, .video-js, .ckplayer, .prism-player, #player, .video-player')) return true; } catch (e) {} try { const iframes = document.querySelectorAll('iframe'); for (const f of iframes) { const src = (f.src || '').toLowerCase(); if (/player|video|\.m3u8|media/.test(src)) return true; } } catch (e) {} return false; } // --- 页面类型识别 --- function detectPageType() { const hasVideo = hasVideoPlayer(); // 作业页识别:URL 含 question / 页面有题目容器 const url = location.href.toLowerCase(); const hasHomeworkContainer = document.querySelector('.e-q, .questionLi, .TiMu, .wrappercontext'); const isHomeworkPage = document.title.includes('作业') || document.querySelector('a[title*="作业"]') || document.querySelector('i.icon-yuan') || url.includes('type=question') || hasHomeworkContainer; const hasNextBtn = !!document.querySelector('#focus > div.preNext.next') || Array.from(document.querySelectorAll('a, button')).some(el => el.textContent.includes('下一单元') || el.textContent.includes('下一节')); let newType; if (hasVideo) { newType = 'video'; } else if (isHomeworkPage) { newType = 'homework'; } else if (hasNextBtn) { newType = 'text'; } else { newType = 'unknown'; } if (newType !== state.pageType) { const prev = state.pageType; state.pageType = newType; const typeText = { video: '视频课程页', text: '文本/教案页', homework: '作业页面(仅目录跳转)', unknown: '未知页面' }; log(`页面类型识别:${typeText[newType]}`); if (newType === 'video') { if (prev === 'text' || prev === 'unknown') { cancelPendingJump(); log("✅ 已取消文本页跳转,按视频页处理"); } updateVideo(); } } return state.pageType; } // --- 判断章节是否已浏览/已学习 --- function isSectionStudied(link) { const parentLi = link.closest('li'); if (!parentLi) return false; if (parentLi.classList.contains('current')) return true; const hasCompleteIcon = parentLi.querySelector('i.iconfont.icon-iconfontyuan[title*="已完成"]') !== null; const hasCompleteText = (link.title || '').includes('已完成') || link.textContent.includes('已完成'); return hasCompleteIcon || hasCompleteText; } // --- 查找并点击第一个未浏览的章节 --- function findAndClickUnstudiedSection() { expandAllFolder(); const linkNodes = document.querySelectorAll( 'ul.list-order li a[href*="/study/html/content/studying/"]:not([href*="javascript:void(0)"])' ); const allLinks = Array.from(linkNodes); if (allLinks.length === 0) { log("❌ 未扫描到任何课程链接"); return false; } const unstudiedLink = allLinks.find(link => !isSectionStudied(link)); if (unstudiedLink) { log(`🔍 找到未浏览内容:${unstudiedLink.title || unstudiedLink.textContent.trim()}`); unstudiedLink.click(); return true; } else { log("✅ 目录内所有内容均已浏览完成"); return false; } } // --- 核心:目录顺序跳转 --- function jumpFromDirectory() { expandAllFolder(); const linkNodes = document.querySelectorAll( 'ul.list-order li a[href*="/study/html/content/studying/"]:not([href*="javascript:void(0)"])' ); const allLinks = Array.from(linkNodes); if (allLinks.length === 0) { log("❌ 未扫描到任何课程链接"); return false; } let currIndex = -1; const currentLi = document.querySelector('li.current'); if (currentLi) { const currA = currentLi.querySelector('a[href*="/study/html/content/studying/"]'); if (currA) { currIndex = allLinks.findIndex(link => link.href === currA.href); } } if (currIndex === -1) { const nowUrl = location.href; currIndex = allLinks.findIndex(link => link.href === nowUrl); } if (currIndex === -1 || currIndex >= allLinks.length - 1) { log("当前位置异常/已到末尾,自动查找未浏览内容"); return findAndClickUnstudiedSection(); } const nextLink = allLinks[currIndex + 1]; if (isSectionStudied(nextLink)) { log(`⚠️ 下一项 ${nextLink.title} 已浏览,继续查找未浏览内容`); return findAndClickUnstudiedSection(); } log(`➡ 顺序跳转至下一项:${nextLink.title || nextLink.textContent.trim()}`); nextLink.click(); return true; } // --- 统一跳转入口 --- function jumpNextManually() { expandAllFolder(); const nextBtn = document.querySelector('#focus > div.preNext.next') || Array.from(document.querySelectorAll('a, button')).find(el => el.textContent.includes('下一单元') || el.textContent.includes('下一节') ); if (nextBtn) { log("点击页面内置【下一节】按钮"); nextBtn.click(); return; } log("页面无内置跳转按钮,使用目录顺序跳转"); jumpFromDirectory(); } // --- 取消待执行的跳转 --- function cancelPendingJump() { if (state.textConfirmTimer) { clearTimeout(state.textConfirmTimer); state.textConfirmTimer = null; } if (state.textJumpTimer) { clearTimeout(state.textJumpTimer); state.textJumpTimer = null; } state.hasProcessedTextPage = false; } // --- 构建控制面板 --- function createPanel() { const panel = document.createElement('div'); panel.id = 'gemini-panel'; if (state.isCollapsed) panel.classList.add('collapsed'); panel.style.left = state.pos.left; panel.style.top = state.pos.top; panel.innerHTML = `
安徽继续教育小助手 V1.3
${state.isCollapsed ? '🚀' : '➖'}
倍速: ${state.currentSpeed}x
音量
视频自动跳转
作业页自动跳转
自动切下一门未完成课程
正在启动...
作业页仅目录跳转 | 不提交作业
`; document.body.appendChild(panel); state.panel = panel; bindEvents(); } // --- 绑定面板交互事件 --- function bindEvents() { document.getElementById('closeBtn').onclick = (e) => { e.stopPropagation(); toggleCollapse(); }; state.panel.onclick = () => { if(state.isCollapsed) toggleCollapse(); }; document.getElementById('speedRange').oninput = (e) => { const val = parseFloat(e.target.value); state.currentSpeed = val; document.getElementById('speedVal').innerText = val; updateVideo(); GM_setValue('playbackSpeed', val); }; document.getElementById('volumeRange').oninput = (e) => { state.volumeLevel = parseFloat(e.target.value); updateVideo(); GM_setValue('volumeLevel', state.volumeLevel); }; document.getElementById('jumpToggle').onchange = (e) => { state.isVideoAutoJumpEnabled = e.target.checked; GM_setValue('isVideoAutoJumpEnabled', state.isVideoAutoJumpEnabled); log(state.isVideoAutoJumpEnabled ? "✅ 开启视频自动跳转" : "❌ 关闭视频自动跳转"); }; document.getElementById('homeworkJumpToggle').onchange = (e) => { state.isHomeworkAutoJumpEnabled = e.target.checked; GM_setValue('isHomeworkAutoJumpEnabled', state.isHomeworkAutoJumpEnabled); log(state.isHomeworkAutoJumpEnabled ? "✅ 开启作业页自动跳转" : "❌ 关闭作业页自动跳转"); }; document.getElementById('playBtn').onclick = () => { const v = getVideoEl(); if(v) { v.play().catch(()=>{}); log("尝试强制播放视频"); } else { log("当前页面无视频播放器"); } }; document.getElementById('manualNextBtn').onclick = jumpNextManually; document.getElementById('nextCourseToggle').onchange = (e) => { state.autoNextCourse = e.target.checked; GM_setValue('autoNextCourse', state.autoNextCourse); log(state.autoNextCourse ? "✅ 开启自动切下一门课" : "❌ 关闭自动切下一门课"); }; document.getElementById('contactQQBtn').onclick = () => { const qqUrl = "https://qm.qq.com/q/fpVawk4WgS"; window.open(qqUrl, "_blank"); log("💬 打开QQ加好友"); }; makeDraggable(state.panel); } // --- 面板折叠/展开 --- function toggleCollapse() { state.isCollapsed = !state.isCollapsed; GM_setValue('panelCollapsed', state.isCollapsed); state.panel.classList.toggle('collapsed'); document.getElementById('closeBtn').innerText = state.isCollapsed ? '🚀' : '➖'; if (!state.isCollapsed && parseInt(state.panel.style.left) < 0) { state.panel.style.left = '10px'; } } // --- 面板拖动 + 边缘吸附 --- function makeDraggable(el) { let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; const header = document.getElementById('panelHeader'); header.onmousedown = (e) => { e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = () => { document.onmouseup = null; document.onmousemove = null; const distLeft = el.offsetLeft; const distRight = window.innerWidth - (el.offsetLeft + el.offsetWidth); if (distLeft < 50) el.style.left = '0px'; if (distRight < 50) el.style.left = (window.innerWidth - el.offsetWidth) + 'px'; GM_setValue('panelPos', {left: el.style.left, top: el.style.top}); }; document.onmousemove = (e) => { e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; el.style.top = (el.offsetTop - pos2) + "px"; el.style.left = (el.offsetLeft - pos1) + "px"; }; }; } // --- 视频倍速、音量、播放维护 --- function updateVideo() { const video = getVideoEl(); if (video) { try { video.playbackRate = state.currentSpeed; video.volume = state.volumeLevel; if (video.paused && !video.ended) { video.play().catch(()=>{}); } } catch (e) {} } } // --- 修复:浏览器缩小/切后台后视频暂停 --- function keepVideoPlaying() { const video = getVideoEl(); if (!video) return; if (video.paused && !video.ended) { video.play().catch(()=>{}); log("🔄 检测到视频暂停,自动恢复播放"); } // 同步倍速 if (Math.abs(video.playbackRate - state.currentSpeed) > 0.1) { video.playbackRate = state.currentSpeed; } } // --- 视频播放完毕自动跳转 --- function checkEnd() { if (state.pageType !== 'video' || !state.isVideoAutoJumpEnabled) return; if (state.endJumpScheduled) return; const video = getVideoEl(); if (video && video.ended) { state.endJumpScheduled = true; log("📹 视频播放完成,3秒后跳转"); setTimeout(() => { state.endJumpScheduled = false; jumpNextManually(); }, 3000); } } // --- 文本页延时跳转 --- function scheduleTextJump() { if (state.hasProcessedTextPage) return; state.hasProcessedTextPage = true; const delay = Math.floor(Math.random() * 10000) + 10000; const delaySec = (delay / 1000).toFixed(1); log(`📄 文本页面,${delaySec}秒后跳转目录下一项`); state.textJumpTimer = setTimeout(() => jumpNextManually(), delay); } function checkTextPageAutoJump() { if (state.pageType !== 'text' || !state.isVideoAutoJumpEnabled) return; if (state.hasProcessedTextPage) return; if (state.textConfirmTimer) return; state.textConfirmTimer = setTimeout(() => { state.textConfirmTimer = null; if (hasVideoPlayer()) { log("📹 复查发现视频播放器,本页按视频页处理"); state.pageType = 'video'; cancelPendingJump(); updateVideo(); return; } scheduleTextJump(); }, 3500); } // --- 作业页跳转逻辑(修复:增加重试和更宽的识别)--- function checkHomeworkAutoJump() { if (state.pageType !== 'homework' || !state.isVideoAutoJumpEnabled || !state.isHomeworkAutoJumpEnabled) return; if (state.homeworkProcessed) return; state.homeworkProcessed = true; const delay = Math.floor(Math.random() * 5000) + 8000; const delaySec = (delay / 1000).toFixed(1); log(`📝 作业页面,${delaySec}秒后跳转目录下一项(不提交作业)`); setTimeout(() => { if (state.pageType === 'homework') { log("➡ 作业完成,API跳转下一单元"); apiJumpNext(); } }, delay); } function handleHomeworkPage() { if (state.pageType !== 'homework') return; if (state.isHomeworkAutoJumpEnabled) { checkHomeworkAutoJump(); } else if (!state.homeworkLogged) { state.homeworkLogged = true; log("📝 作业页面:已关闭作业自动跳转,停留在当前页面"); } } // --- 监听视频元素注入 --- function watchForVideoInjection() { if (!window.MutationObserver) return; try { const mo = new MutationObserver(() => { if (hasVideoPlayer() && (state.pageType === 'text' || state.pageType === 'unknown')) { log("📹 检测到播放器注入,立即切换为视频页并取消文本跳转"); cancelPendingJump(); state.pageType = 'video'; updateVideo(); } }); mo.observe(document.body, { childList: true, subtree: true }); } catch (e) {} } // --- 课程列表页:自动点未完成课程 --- function handleCourseListPage() { if (!state.autoNextCourse) return; // 找未完成的课程卡片 const cards = document.querySelectorAll('.course-item, .card, .list-item, [class*="course"]'); for (const card of cards) { const txt = card.innerText || ''; if (txt.includes('未完成') || txt.includes('学习中') || txt.includes('进行中')) { const link = card.querySelector('a[href*="courseOpenId"]') || card.querySelector('a'); if (link) { log('➡ 自动进入课程:' + (card.innerText || '').slice(0, 30)); link.click(); return; } } } } function isCourseListPage() { return location.href.includes('/studentstudio/course/studying'); } // --- 全局定时轮询主逻辑 --- function mainLoop() { if (isCourseListPage()) { handleCourseListPage(); return; } if (location.href !== state.lastUrl) { state.lastUrl = location.href; state.hasProcessedTextPage = false; state.homeworkProcessed = false; state.homeworkLogged = false; state.endJumpScheduled = false; cancelPendingJump(); expandAllFolder(); detectPageType(); } else { detectPageType(); } updateVideo(); // 每 5 秒检查一次视频是否被暂停(浏览器缩小/切后台) const now = Date.now(); if (now - state.lastVideoCheck > 5000) { state.lastVideoCheck = now; keepVideoPlaying(); } checkEnd(); checkTextPageAutoJump(); handleHomeworkPage(); } // --- 初始化 --- function init() { createPanel(); expandAllFolder(); detectPageType(); log("✅ 脚本 V1.3 初始化完成 | 修复作业跳转 + 视频暂停保活"); setInterval(mainLoop, 3000); setInterval(() => { const dialogBtn = document.querySelector('.layui-layer-btn0, .confirm'); if (dialogBtn) { log("🔔 自动关闭验证弹窗"); dialogBtn.click(); } }, 5000); watchForVideoInjection(); // 切回前台时恢复播放 document.addEventListener('visibilitychange', () => { if (!document.hidden) { setTimeout(keepVideoPlaying, 500); } }); window.addEventListener('resize', () => { setTimeout(keepVideoPlaying, 500); }); window.addEventListener('focus', () => { setTimeout(keepVideoPlaying, 500); }); } window.addEventListener('load', init); })();