// ==UserScript== // @name 学达云教育自动学习脚本 // @namespace http://tampermonkey.net/ // @version 1.8 // @description 支持总课表自动选择和课程学习自动切换 // @author Auto Learning Script // @match *://*.ok99ok99.com/stu/study_new_v3.aspx* // @match *://ok99ok99.com/stu/study_new_v3.aspx* // @match *://*.ok99ok99.com/stu/cls_courselist.aspx* // @match *://ok99ok99.com/stu/cls_courselist.aspx* // @grant none // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // 判断当前页面类型 const isCourseListPage = location.href.includes('cls_courselist.aspx'); const isStudyPage = location.href.includes('study_new_v3.aspx'); // ==================== 日志系统 ==================== const LOG_KEY = 'autoLearning_logs'; let logs = []; function log(msg, type = 'info') { const time = new Date().toLocaleTimeString(); const entry = `[${time}] [${type.toUpperCase()}] ${msg}`; logs.push(entry); if (logs.length > 100) logs.shift(); try { localStorage.setItem(LOG_KEY, JSON.stringify(logs)); } catch (e) {} const styles = { info: 'color: #2196F3', success: 'color: #4CAF50', warning: 'color: #FF9800', error: 'color: #F44336' }; console.log(`%c[自动学习] ${msg}`, styles[type] || ''); updateLogUI(entry, type); } function getLogs() { try { return JSON.parse(localStorage.getItem(LOG_KEY)) || logs; } catch (e) { return logs; } } function clearLogs() { logs = []; localStorage.removeItem(LOG_KEY); log('日志已清空', 'info'); } // ==================== 配置 ==================== const DEFAULT_CONFIG = { checkInterval: 10000, submitDelay: 10000, nextCourseDelay: 10000, refreshDelay: 10000, videoPlayRetry: 10000, autoStart: true, maxSwitchAttempts: 3 }; let CONFIG = { ...DEFAULT_CONFIG }; function loadConfig() { try { const saved = localStorage.getItem('autoLearning_config'); if (saved) CONFIG = { ...DEFAULT_CONFIG, ...JSON.parse(saved) }; } catch (e) {} } function saveConfig() { try { localStorage.setItem('autoLearning_config', JSON.stringify(CONFIG)); } catch (e) {} } // ==================== 状态 ==================== let state = { isRunning: false, timer: null, switchAttempts: 0, lastCourseName: '', submitButtonWasDisabled: false, // 新增:记录提交按钮是否曾经可用过 hasSubmitted: false // 新增:记录是否已成功提交 }; // ==================== 工具函数 ==================== function parseTime(str) { if (!str) return 0; const parts = str.split(':').map(Number); return parts.length === 3 ? parts[0] * 3600 + parts[1] * 60 + parts[2] : 0; } function formatTime(sec) { const h = Math.floor(sec / 3600); const m = Math.floor((sec % 3600) / 60); const s = sec % 60; return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; } function getStudyTime() { const h = document.querySelector('.study_time_hour')?.textContent || '0'; const m = document.querySelector('.study_time_minute')?.textContent || '0'; const s = document.querySelector('.study_time_second')?.textContent || '0'; return parseInt(h) * 3600 + parseInt(m) * 60 + parseInt(s); } function getPlanTime() { return parseTime(document.querySelector('.plan_time')?.textContent || '0'); } function getCurrentCourseName() { return document.querySelector('.chapter_name')?.textContent?.trim() || '未知'; } // ==================== 核心功能 ==================== function canSubmit() { const study = getStudyTime(); const plan = getPlanTime(); return study >= plan && plan > 0; } // 检查课程是否已完成(简化逻辑:学习时间>=计划时间 且 提交按钮禁用) function isCourseFinished() { const study = getStudyTime(); const plan = getPlanTime(); // 方式1: 检查完成提示是否显示 const finishedDiv = document.querySelector('.study_finished'); if (finishedDiv && finishedDiv.style.display !== 'none') { log('检测到完成提示显示', 'success'); return true; } // 方式2: 学习时间已达标且提交按钮被禁用 const submitBtn = document.querySelector('.submit_btn'); if (study >= plan && plan > 0 && submitBtn && submitBtn.disabled) { log(`时间已达标(${formatTime(study)} >= ${formatTime(plan)}) 且提交按钮已禁用,判定为已完成`, 'success'); return true; } // 方式3: 已成功提交过 if (state.hasSubmitted && study >= plan && plan > 0) { log('已成功提交且时间已达标,判定为已完成', 'success'); return true; } return false; } function submit() { log('尝试提交...', 'info'); const btn = document.querySelector('.submit_btn'); if (!btn) { log('未找到提交按钮!', 'error'); return false; } log(`提交按钮状态: disabled=${btn.disabled}`, 'info'); if (btn.disabled) { log('提交按钮被禁用,可能已提交过', 'warning'); return false; } btn.click(); log('已点击提交按钮', 'success'); state.hasSubmitted = true; return true; } function getAllCourses() { const items = document.querySelectorAll('#chapter_tree .layui-tree-set'); log(`找到 ${items.length} 个课程项`, 'info'); const courses = []; items.forEach((item, index) => { const dataId = item.getAttribute('data-id'); const txt = item.querySelector('.layui-tree-txt'); if (!txt) return; const text = txt.textContent.trim(); const hasGreenCheck = txt.querySelector('.ico_dui.color_green'); const hasGreenText = txt.querySelector('.color_green'); const hasVideoIcon = txt.querySelector('.ico_shipin2'); const isFinished = hasGreenCheck || hasGreenText; log(`课程${index}: "${text.substring(0,15)}" 已完成=${!!isFinished} 视频=${!!hasVideoIcon}`, 'info'); courses.push({ index, dataId, text, isFinished: !!isFinished, hasVideo: !!hasVideoIcon, element: item }); }); return courses; } function getNextUnfinishedCourse() { log('查找下一节未完成课程...', 'info'); const courses = getAllCourses(); const unfinished = courses.filter(c => !c.isFinished && c.hasVideo); log(`未完成课程数量: ${unfinished.length}`, 'info'); if (unfinished.length === 0) { log('没有未完成的视频课程', 'warning'); return null; } const next = unfinished[0]; log(`下一节课程: "${next.text.substring(0, 30)}"`, 'success'); return next; } function switchCourse() { log('========== 开始切换课程 ==========', 'info'); const next = getNextUnfinishedCourse(); if (!next) { log('没有更多课程,学习完成!', 'success'); stop(); return false; } const item = next.element; let clickTarget = null; const selectors = ['.layui-tree-main', '.layui-tree-txt', '.layui-tree-entry']; for (const sel of selectors) { clickTarget = item.querySelector(sel); if (clickTarget) { log(`找到可点击元素: ${sel}`, 'info'); break; } } if (!clickTarget) { log('未找到可点击元素,尝试点击整个项', 'warning'); clickTarget = item; } state.lastCourseName = getCurrentCourseName(); log(`当前课程: "${state.lastCourseName}"`, 'info'); log(`目标课程: "${next.text.substring(0, 30)}"`, 'info'); try { clickTarget.click(); log('已执行点击操作', 'success'); // 重置状态 state.switchAttempts = 0; state.hasSubmitted = false; // 等待课程切换完成后再返回结果 return new Promise((resolve) => { setTimeout(() => { const newCourse = getCurrentCourseName(); log(`点击后当前课程: "${newCourse}"`, 'info'); if (newCourse !== state.lastCourseName) { log('课程切换成功!', 'success'); setTimeout(playVideo, 2000); resolve(true); } else { log('课程切换失败,当前课程未变化', 'warning'); resolve(false); } }, 2000); }); } catch (e) { log('点击失败: ' + e.message, 'error'); return false; } } function playVideo() { log('尝试播放视频...', 'info'); const videoContainer = document.querySelector('.course_video'); if (videoContainer && videoContainer.style.display !== 'none') { log('检测到视频内容', 'info'); const iframe = document.querySelector('#iframeplay'); if (iframe) { try { const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; const video = iframeDoc.querySelector('video'); if (video) video.play().catch(() => {}); iframeDoc.querySelectorAll('.vjs-big-play-button, .play-button, [class*="play"]') .forEach(btn => btn.click()); } catch (e) {} } const video = videoContainer.querySelector('video'); if (video) video.play().catch(() => {}); videoContainer.querySelectorAll('[class*="play"], .vjs-big-play-button') .forEach(btn => btn.click()); } } function refreshPage() { log('========== 准备刷新页面 ==========', 'warning'); localStorage.setItem('autoLearning_needSwitch', 'true'); localStorage.setItem('autoLearning_switchTime', Date.now().toString()); setTimeout(() => { log('执行刷新...', 'warning'); location.reload(); }, CONFIG.refreshDelay); } function checkAutoSwitch() { const needSwitch = localStorage.getItem('autoLearning_needSwitch'); const switchTime = localStorage.getItem('autoLearning_switchTime'); if (needSwitch === 'true' && switchTime) { const elapsed = Date.now() - parseInt(switchTime); if (elapsed < 60000) { localStorage.removeItem('autoLearning_needSwitch'); localStorage.removeItem('autoLearning_switchTime'); log('检测到刷新标记,3秒后自动切换', 'info'); setTimeout(switchCourse, 3000); } } } // ==================== 主循环 ==================== function handleCourseFinished() { log('课程已完成,准备切换下一节', 'success'); const result = switchCourse(); if (result && typeof result.then === 'function') { // 返回 Promise,等待切换结果 result.then(success => { if (!success) { handleSwitchFailure(); } }); } else if (!result) { handleSwitchFailure(); } } function handleSwitchFailure() { state.switchAttempts++; log(`切换失败,尝试次数: ${state.switchAttempts}/${CONFIG.maxSwitchAttempts}`, 'warning'); if (state.switchAttempts < CONFIG.maxSwitchAttempts) { refreshPage(); } else { log('达到最大重试次数,停止', 'error'); stop(); } } function check() { if (!state.isRunning) return; const study = getStudyTime(); const plan = getPlanTime(); const currentCourse = getCurrentCourseName(); log(`--- 检查 --- 课程: "${currentCourse.substring(0,15)}" 学习: ${formatTime(study)} 计划: ${formatTime(plan)}`, 'info'); updatePanel(); // 检查课程是否已完成 if (isCourseFinished()) { handleCourseFinished(); return; } // 检查是否可以提交 if (canSubmit()) { log('学习时间达标,准备提交', 'success'); setTimeout(() => { if (submit()) { log('提交成功', 'success'); // 提交成功后等待一段时间,然后检查是否需要切换课程 setTimeout(() => { if (isCourseFinished()) { handleCourseFinished(); } }, CONFIG.nextCourseDelay); } else { // 提交按钮被禁用,可能已提交过 log('提交按钮禁用,检查是否已完成', 'warning'); if (isCourseFinished()) { handleCourseFinished(); } } }, CONFIG.submitDelay); } } function start() { if (state.isRunning) return; state.isRunning = true; state.switchAttempts = 0; state.hasSubmitted = false; state.submitButtonWasDisabled = false; log('========== 开始自动学习 ==========', 'success'); updateStatus('学习中...'); updateIndicator(true); state.timer = setInterval(check, CONFIG.checkInterval); check(); setTimeout(playVideo, 2000); } function stop() { state.isRunning = false; if (state.timer) { clearInterval(state.timer); state.timer = null; } log('========== 停止自动学习 ==========', 'warning'); updateStatus('已停止'); updateIndicator(false); } // ==================== UI ==================== function createPanel() { if (document.getElementById('al-panel')) return; const css = document.createElement('style'); css.textContent = ` #al-panel{position:fixed;top:10px;right:10px;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;padding:12px 16px;border-radius:10px;box-shadow:0 4px 16px rgba(0,0,0,.25);z-index:999999;font:13px/1.4 'Microsoft YaHei',sans-serif;min-width:280px;max-height:90vh;overflow-y:auto} #al-panel *{box-sizing:border-box} #al-panel h3{margin:0 0 8px;padding-bottom:6px;border-bottom:1px solid rgba(255,255,255,.3);display:flex;justify-content:space-between;align-items:center} #al-panel .row{display:flex;justify-content:space-between;margin:5px 0} #al-panel .val{color:#FFD700;font-weight:700;max-width:160px;overflow:hidden;text-overflow:ellipsis} #al-panel .bar{height:8px;background:rgba(255,255,255,.3);border-radius:4px;margin:8px 0;overflow:hidden} #al-panel .bar-in{height:100%;background:#FFD700;transition:width .3s} #al-panel .status{text-align:center;padding:4px;background:rgba(0,0,0,.15);border-radius:4px;margin:6px 0;font-size:12px} #al-panel .dot{display:inline-block;width:6px;height:6px;background:#4CAF50;border-radius:50%;margin-right:4px;animation:blink 1s infinite} @keyframes blink{0%,100%{opacity:1}50%{opacity:.4}} #al-panel .btns{display:flex;gap:6px;margin-top:8px} #al-panel button{flex:1;padding:6px 10px;border:none;border-radius:4px;cursor:pointer;font:inherit;font-size:12px;font-weight:700;color:#fff} #al-panel .btn1{background:#4CAF50}#al-panel .btn2{background:#f44336} #al-panel .btn3{background:#2196F3}#al-panel .btn4{background:#FF9800} #al-panel .btn5{background:#9C27B0}#al-panel .btn6{background:#607D8B} #al-panel button:hover{opacity:.9} #al-panel .set-icon{cursor:pointer;opacity:.8;font-size:16px} #al-panel .set-icon:hover{opacity:1} #al-panel .log-box{margin-top:10px;background:rgba(0,0,0,.2);border-radius:6px;max-height:150px;overflow-y:auto} #al-panel .log-title{padding:6px 8px;font-size:12px;border-bottom:1px solid rgba(255,255,255,.2);display:flex;justify-content:space-between;align-items:center} #al-panel .log-content{padding:4px 8px;font-size:10px;font-family:monospace;max-height:120px;overflow-y:auto} #al-panel .log-item{padding:2px 0;border-bottom:1px solid rgba(255,255,255,.1);word-break:break-all} #al-panel .log-item.info{color:#90CAF9}#al-panel .log-item.success{color:#A5D6A7} #al-panel .log-item.warning{color:#FFCC80}#al-panel .log-item.error{color:#EF9A9A} #al-set{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;color:#333;padding:16px;border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.3);z-index:9999999;min-width:340px;font:13px/1.4 'Microsoft YaHei',sans-serif} #al-set h4{margin:0 0 12px;padding-bottom:8px;border-bottom:2px solid #667eea;color:#667eea;display:flex;justify-content:space-between} #al-set .s-row{display:flex;justify-content:space-between;align-items:center;margin:8px 0} #al-set .s-row input{width:80px;padding:4px 8px;border:1px solid #ddd;border-radius:4px;text-align:center} #al-set .s-row input:focus{border-color:#667eea;outline:none} #al-set .hint{font-size:11px;color:#999;margin-top:2px} #al-set .cb{display:flex;align-items:center;gap:6px;margin:8px 0} #al-set .cb input{width:14px;height:14px} #al-set .s-btns{display:flex;gap:8px;margin-top:12px;padding-top:10px;border-top:1px solid #eee} #al-set .s-btns button{flex:1;padding:8px;border:none;border-radius:4px;cursor:pointer;font:inherit;font-weight:700;color:#fff} #al-set .sv{background:#4CAF50}#al-set .rs{background:#FF9800}#al-set .cl{background:#9E9E9E} #al-set .bg{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.4);z-index:-1} `; document.head.appendChild(css); const panel = document.createElement('div'); panel.id = 'al-panel'; panel.innerHTML = `