// ==UserScript==
// @name 山东教育教师网 - 自动学习脚本
// @namespace https://github.com/your-repo/shandong-edu-auto-study
// @version 3.0.0
// @description 在山东教育教师网学习网课时,完成学习后自动跳转到下一课程
// @author Your Name
// @match *://*.qlteacher.com/*
// @match *://*.sdedu.gov.cn/*
// @match *://*.sdteacher.com/*
// @grant GM_notification
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// ==================== 配置区域 ====================
const CONFIG = {
debug: true,
jumpDelay: 2000, // 跳转延迟(毫秒)
checkInterval: 1000, // 检查间隔(毫秒)
selectors: {
countdown: '.count-down',
chapterItem: '.ant-list-item',
videoPlayer: 'video',
checkIcon: '.anticon-check-circle',
ellipsis: 'ELLIPSIS',
},
logPrefix: '[山东教育自动学习]',
};
// ==================== 样式 ====================
function addStyles() {
var css = `
#auto-study-panel {
position: fixed;
top: 10px;
right: 10px;
width: 320px;
background: #fff;
border: 2px solid #1890ff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
z-index: 99999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
}
#auto-study-panel.collapsed {
width: 120px;
height: 40px;
overflow: hidden;
}
#auto-study-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
padding: 10px 15px;
border-radius: 6px 6px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
user-select: none;
}
#auto-study-panel.collapsed #auto-study-header {
border-radius: 6px;
}
#auto-study-title {
font-weight: bold;
font-size: 14px;
}
#auto-study-toggle {
background: rgba(255,255,255,0.2);
border: none;
color: #fff;
padding: 2px 8px;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
}
#auto-study-toggle:hover {
background: rgba(255,255,255,0.3);
}
#auto-study-content {
padding: 12px 15px;
max-height: 400px;
overflow-y: auto;
}
.status-item {
margin-bottom: 10px;
padding: 8px;
background: #f5f5f5;
border-radius: 4px;
}
.status-label {
color: #666;
font-size: 11px;
margin-bottom: 3px;
}
.status-value {
color: #333;
font-size: 13px;
font-weight: 500;
word-break: break-all;
}
.status-value.success {
color: #52c41a;
}
.status-value.warning {
color: #faad14;
}
.status-value.error {
color: #f5222d;
}
`;
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
log('✓ 样式已添加');
}
// ==================== UI 面板 ====================
var panelElement = null;
function createPanel() {
if (panelElement) return;
panelElement = document.createElement('div');
panelElement.id = 'auto-study-panel';
panelElement.innerHTML = `
`;
document.body.appendChild(panelElement);
// 绑定事件
document.getElementById('auto-study-toggle').addEventListener('click', function() {
panelElement.classList.toggle('collapsed');
this.textContent = panelElement.classList.contains('collapsed') ? '展开' : '收起';
});
// 添加拖动功能
makeDraggable(panelElement, document.getElementById('auto-study-header'));
log('✓ UI 面板已创建(可拖动)');
}
function makeDraggable(element, handle) {
var isDragging = false;
var currentX = 0;
var currentY = 0;
var initialX = 0;
var initialY = 0;
handle.addEventListener('mousedown', dragStart);
function dragStart(e) {
// 如果点击的是按钮,不触发拖动
if (e.target.tagName === 'BUTTON') return;
isDragging = true;
initialX = e.clientX - currentX;
initialY = e.clientY - currentY;
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
}
function drag(e) {
if (!isDragging) return;
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
// 限制在窗口内
var maxX = window.innerWidth - element.offsetWidth;
var maxY = window.innerHeight - element.offsetHeight;
currentX = Math.max(0, Math.min(currentX, maxX));
currentY = Math.max(0, Math.min(currentY, maxY));
element.style.transform = 'translate(' + currentX + 'px, ' + currentY + 'px)';
}
function dragEnd() {
isDragging = false;
document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', dragEnd);
}
}
function updateUI() {
if (!panelElement) return;
// 更新当前章节
var statusChapter = document.getElementById('status-chapter');
var incompleteChapter = findFirstIncompleteChapter();
if (incompleteChapter) {
statusChapter.textContent = '[' + incompleteChapter.index + '] ' + incompleteChapter.text.substring(0, 25);
} else {
statusChapter.textContent = '所有章节已完成';
statusChapter.className = 'status-value success';
}
// 更新下一跳转章节
var statusNext = document.getElementById('status-next');
var nextChapter = findNextIncompleteChapter();
if (nextChapter) {
statusNext.textContent = '[' + nextChapter.index + '] ' + nextChapter.text.substring(0, 25);
statusNext.className = 'status-value warning';
} else {
statusNext.textContent = '无';
statusNext.className = 'status-value success';
}
// 更新倒计时
var statusCountdown = document.getElementById('status-countdown');
var countdownText = getCountdownText();
if (countdownText) {
statusCountdown.textContent = countdownText;
if (countdownText.includes('已完成')) {
statusCountdown.className = 'status-value success';
} else {
statusCountdown.className = 'status-value';
}
} else {
statusCountdown.textContent = '未找到';
statusCountdown.className = 'status-value warning';
}
}
// ==================== 工具函数 ====================
function log(...args) {
console.log(CONFIG.logPrefix, ...args);
}
function warn(...args) {
console.warn(CONFIG.logPrefix, ...args);
}
function error(...args) {
console.error(CONFIG.logPrefix, ...args);
}
function debug(...args) {
if (CONFIG.debug) {
console.debug(CONFIG.logPrefix, ...args);
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 定时更新UI
var uiUpdateTimer = null;
function startUIUpdate() {
if (uiUpdateTimer) return;
uiUpdateTimer = setInterval(function() {
updateUI();
}, 1000); // 每秒更新一次UI
}
// ==================== 核心功能 ====================
/**
* 检查章节是否有完成图标
*/
function hasCheckIcon(chapter) {
return chapter.querySelector(CONFIG.selectors.checkIcon) !== null;
}
/**
* 检查当前页面是否有视频
*/
function hasVideo() {
return document.querySelector(CONFIG.selectors.videoPlayer) !== null;
}
/**
* 获取倒计时文本
*/
function getCountdownText() {
var countdown = document.querySelector(CONFIG.selectors.countdown);
return countdown ? countdown.textContent.trim() : null;
}
/**
* 检查当前课程是否完成
*/
function isCourseComplete() {
var text = getCountdownText();
if (!text) return false;
return text.includes('已完成') || text.includes('已学完') || text === '0:00';
}
/**
* 查找第一个未完成的章节
*/
function findFirstIncompleteChapter() {
var allChapters = document.querySelectorAll(CONFIG.selectors.chapterItem);
for (var i = 0; i < allChapters.length; i++) {
var chapter = allChapters[i];
if (!hasCheckIcon(chapter)) {
return {
index: i,
element: chapter,
text: chapter.textContent.trim()
};
}
}
return null; // 所有章节都已完成
}
/**
* 查找下一个未完成的章节(跳过当前章节)
*/
function findNextIncompleteChapter() {
var allChapters = document.querySelectorAll(CONFIG.selectors.chapterItem);
var currentChapterId = getCurrentChapterId();
// 如果无法获取当前章节ID,返回第一个未完成的
if (currentChapterId === null) {
return findFirstIncompleteChapter();
}
// 查找当前章节之后的第一个未完成章节
for (var i = 0; i < allChapters.length; i++) {
var chapter = allChapters[i];
if (!hasCheckIcon(chapter) && i > currentChapterId) {
return {
index: i,
element: chapter,
text: chapter.textContent.trim()
};
}
}
// 如果后面没有了,返回第一个未完成的(循环)
for (var j = 0; j < allChapters.length; j++) {
var chapter2 = allChapters[j];
if (!hasCheckIcon(chapter2)) {
return {
index: j,
element: chapter2,
text: chapter2.textContent.trim()
};
}
}
return null; // 所有章节都已完成
}
/**
* 获取当前章节的ID(通过URL)
*/
function getCurrentChapterId() {
var url = window.location.href;
var urlParts = url.split('/');
var currentId = urlParts[urlParts.length - 1];
// 在章节列表中查找匹配的
var allChapters = document.querySelectorAll(CONFIG.selectors.chapterItem);
for (var i = 0; i < allChapters.length; i++) {
var chapter = allChapters[i];
var ellipsis = chapter.querySelector(CONFIG.selectors.ellipsis);
if (ellipsis) {
// 通过点击后的URL来判断(这里简化处理,返回索引)
// 实际上可以通过Angular的路由数据获取
}
}
// 简化:通过倒计时状态判断
// 如果有倒计时且不是"已完成",说明在当前章节
var countdown = getCountdownText();
if (countdown && !countdown.includes('已完成')) {
// 找到第一个未完成的章节(假设就是当前章节)
for (var k = 0; k < allChapters.length; k++) {
if (!hasCheckIcon(allChapters[k])) {
return k;
}
}
}
return null;
}
/**
* 跳转到指定章节
*/
function jumpToChapter(chapterInfo) {
if (!chapterInfo || !chapterInfo.element) {
warn('无法跳转:章节信息无效');
return false;
}
var ellipsis = chapterInfo.element.querySelector(CONFIG.selectors.ellipsis);
if (!ellipsis) {
warn('无法跳转:找不到ELLIPSIS元素');
return false;
}
log('跳转到章节[' + chapterInfo.index + ']:', chapterInfo.text.substring(0, 30));
ellipsis.click();
return true;
}
/**
* 检查并跳转(核心逻辑)
*/
async function checkAndJump() {
debug('========== 检查开始 ==========');
// 1. 检查是否所有章节都已完成
var allChapters = document.querySelectorAll(CONFIG.selectors.chapterItem);
var allCompleted = true;
for (var i = 0; i < allChapters.length; i++) {
if (!hasCheckIcon(allChapters[i])) {
allCompleted = false;
break;
}
}
if (allCompleted) {
log('🎉 所有章节都已完成!');
stopScript();
return;
}
// 2. 检查当前章节是否有视频
if (!hasVideo()) {
log('⚠ 当前章节没有视频,直接跳转');
var incompleteChapter = findFirstIncompleteChapter();
if (incompleteChapter) {
await sleep(CONFIG.jumpDelay);
jumpToChapter(incompleteChapter);
}
return;
}
// 3. 检查倒计时是否完成
if (isCourseComplete()) {
log('✓ 当前课程已完成,查找下一章节');
var incompleteChapter = findFirstIncompleteChapter();
if (incompleteChapter) {
await sleep(CONFIG.jumpDelay);
jumpToChapter(incompleteChapter);
} else {
log('🎉 所有章节都已完成!');
stopScript();
}
} else {
var countdownText = getCountdownText();
debug('当前倒计时:', countdownText);
}
debug('========== 检查结束 ==========');
}
// ==================== 主循环 ====================
let mainTimer = null;
let isRunning = false;
/**
* 主循环
*/
async function mainLoop() {
if (isRunning) {
debug('上一轮检查尚未完成,跳过');
return;
}
isRunning = true;
try {
await checkAndJump();
} catch (e) {
error('主循环出错:', e);
} finally {
isRunning = false;
}
}
/**
* 启动脚本
*/
function startScript() {
log('脚本正在启动...');
// 清除已存在的定时器
if (mainTimer) {
clearInterval(mainTimer);
mainTimer = null;
}
// 立即执行一次检查
mainLoop();
// 设置定时检查
mainTimer = setInterval(mainLoop, CONFIG.checkInterval);
// 启动UI更新
startUIUpdate();
log('✓ 脚本已启动,开始监控...');
}
/**
* 停止脚本
*/
function stopScript() {
if (mainTimer) {
clearInterval(mainTimer);
mainTimer = null;
}
log('脚本已停止');
}
// ==================== 控制台命令 ====================
const safeWindow = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
safeWindow.checkStatus = function() {
log('========== 当前状态 ==========');
log('URL:', location.href);
log('有视频:', hasVideo());
log('倒计时:', getCountdownText());
log('课程完成:', isCourseComplete());
var incomplete = findFirstIncompleteChapter();
if (incomplete) {
log('第一个未完成章节[' + incomplete.index + ']:', incomplete.text.substring(0, 30));
} else {
log('所有章节都已完成');
}
log('==============================');
};
safeWindow.stopScript = function() {
stopScript();
};
safeWindow.startScript = function() {
startScript();
};
// ==================== 初始化 ====================
// 等待页面加载完成后创建UI
if (document.readyState === 'complete') {
addStyles();
createPanel();
startScript();
} else {
window.addEventListener('load', function() {
addStyles();
createPanel();
startScript();
});
}
})();