// ==UserScript==
// @name 国家智慧教育平台-学习助手(优化版)
// @namespace http://tampermonkey.net/
// @version 5.2
// @description 真实结构解析+状态图标;启动强制跳到第一个未完成;轮询切课、跳过已学、进度条、倍速伪装、弹窗自动关、后台保活、可拖动精简面板
// @author 寻道书生
// @match *://basic.smartedu.cn/*
// @match *://www.smartedu.cn/*
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
let config = {
speed: 2.0,
autoScroll: true,
autoNext: true,
autoMute: true,
autoPlay: true,
keepAlive: true,
closeModal: true,
showStat: true,
skipDone: true,
resume: true,
autoStart: true
};
let scrollRan = false;
let bindEnded = false;
let currentVideo = null;
let skipCount = 0;
let curEntry = null;
let globalIdx = -1;
let lastClickEntry = null;
let lastClickTime = 0;
let lastSwitchTime = 0;
let lastUserClickTime = 0;
let observer = null;
// ============ 精简面板 ============
function createPanel(){
const panel = document.createElement('div');
panel.id = "studyPanel";
panel.style=`
position:fixed;top:80px;right:20px;z-index:9999;
background:#fff;border:1px solid #e8e8e8;border-radius:10px;
padding:0;width:250px;font-size:12px;box-shadow:0 4px 16px #0001a;
font-family:system-ui,sans-serif;overflow:hidden;
`;
panel.innerHTML=`
倍速
📊 进度
统计中...
▶ 正在: -
⏭ 下一节: -
`;
document.body.appendChild(panel);
// 拖拽
(function(){
const header = document.getElementById('panelHeader');
if(!header) return;
try{
const pos = JSON.parse(localStorage.getItem('smartedu_panel_pos')||'null');
if(pos){
panel.style.left = pos.left+'px';
panel.style.top = pos.top+'px';
panel.style.right = 'auto';
}
}catch(e){}
let dragging = false, ox = 0, oy = 0;
header.addEventListener('mousedown', (e)=>{
dragging = true;
ox = e.clientX - panel.offsetLeft;
oy = e.clientY - panel.offsetTop;
panel.style.right = 'auto';
e.preventDefault();
});
document.addEventListener('mousemove', (e)=>{
if(!dragging) return;
let x = e.clientX - ox, y = e.clientY - oy;
x = Math.max(0, Math.min(x, window.innerWidth - panel.offsetWidth));
y = Math.max(0, Math.min(y, window.innerHeight - panel.offsetHeight));
panel.style.left = x+'px';
panel.style.top = y+'px';
});
document.addEventListener('mouseup', ()=>{
if(!dragging) return;
dragging = false;
try{
localStorage.setItem('smartedu_panel_pos', JSON.stringify({left: panel.offsetLeft, top: panel.offsetTop}));
}catch(e){}
});
})();
document.getElementById('setSpeedBtn').onclick = ()=>{
const val = parseFloat(document.getElementById('speedInput').value);
if(val>=0.5 && val<=10){
config.speed = val;
applySpeed(currentVideo);
document.getElementById('feedbackLine').textContent = '✅ 倍速已设为 '+val+'x(平台显示1x)';
}else{
document.getElementById('feedbackLine').textContent = '⚠️ 倍速需在 0.5~10 之间';
}
};
document.getElementById('cbAutoStart').onchange = e=>config.autoStart = e.target.checked;
document.getElementById('cbNext').onchange = e=>config.autoNext = e.target.checked;
document.getElementById('cbScroll').onchange = e=>config.autoScroll = e.target.checked;
document.getElementById('cbKeep').onchange = e=>config.keepAlive = e.target.checked;
document.getElementById('locateBtn').onclick = ()=>{
const cur = findCurrentEntry(true);
if(cur){
document.getElementById('feedbackLine').textContent = '🎯 已定位第'+(globalIdx+1)+'节: '+(cur.textContent||'').replace(/\s+/g,' ').trim().slice(0,26);
} else {
document.getElementById('feedbackLine').textContent = '⚠️ 未定位到当前课,请先点开一节课';
}
refreshNowLine();
};
document.getElementById('diagBtn').onclick = ()=>{
const lines = diagCourses();
const ta = document.getElementById('diagText');
ta.value = lines;
document.getElementById('diagArea').style.display = '';
document.getElementById('copyBtn').style.display = '';
document.getElementById('feedbackLine').textContent = '已输出 '+lines.split('\n').length+' 行,复制后发我';
};
document.getElementById('copyBtn').onclick = ()=>{
const ta = document.getElementById('diagText');
ta.select();
ta.setSelectionRange(0, 99999);
try{
document.execCommand('copy');
document.getElementById('feedbackLine').textContent = '✅ 已复制,粘贴发给助手即可';
}catch(e){
document.getElementById('feedbackLine').textContent = '复制失败,请手动全选复制';
}
};
document.getElementById('clearBtn').onclick = ()=>{
try{
localStorage.removeItem('smartedu_progress');
localStorage.removeItem('smartedu_played');
}catch(e){}
globalIdx = -1; curEntry = null; skipCount = 0;
document.getElementById('feedbackLine').textContent = '🧹 已清除断点和已播记录';
updateStat();
}
}
// ============ 目录解析(真实结构) ============
function getAllEntries(){
const list = [];
document.querySelectorAll('.resource-item.resource-item-train').forEach(el=>{
const t = (el.textContent||'').trim();
if(t.length < 2) return;
if(el.querySelector('video,audio')) return;
list.push(el);
});
return list;
}
function getModulePath(el){
const parts = [];
let p = el.parentElement;
while(p && p !== document.body){
if(p.classList && p.classList.contains('fish-collapse-item')){
const h = p.querySelector(':scope > .fish-collapse-header');
if(h && (h.textContent||'').trim()){
parts.unshift((h.textContent||'').trim().slice(0,18));
}
}
p = p.parentElement;
}
return parts.join('>');
}
function getItemState(el){
if(!el) return 'todo';
if(el.querySelector('.icon_checkbox_fill')) return 'done';
if(el.querySelector('.icon_processing_fill')) return 'processing';
return 'todo';
}
function isDone(el){
if(getItemState(el) === 'done') return true;
return getPlayedSet().has((el.textContent||'').trim());
}
function isUnfinished(el){
return el && !isDone(el);
}
function ensureEntryVisible(entry){
const headers = [];
let p = entry.parentElement;
while(p){
if(p.classList && p.classList.contains('fish-collapse-item')){
const h = p.querySelector(':scope > .fish-collapse-header');
if(h && !/active/.test(h.className)) headers.unshift(h);
}
p = p.parentElement;
if(p && p.classList && p.classList.contains('fish-collapse')) break;
}
headers.forEach(h=>h.click());
}
// ============ 定位 ============
function findCurrentEntry(forceCalib){
const all = getAllEntries();
const active = document.querySelector('.resource-item-active');
if(active){
const i = all.indexOf(active);
if(i >= 0){
globalIdx = i;
curEntry = active;
return active;
}
}
if(globalIdx >= 0 && all[globalIdx] && document.body.contains(all[globalIdx])){
curEntry = all[globalIdx];
return curEntry;
}
if(forceCalib && all.length) return null;
return null;
}
function findFirstUnfinished(fromIdx){
const all = getAllEntries();
const start = fromIdx >= 0 ? fromIdx : 0;
for(let i=start; i= 0) globalIdx = idx;
curEntry = entry;
verifyPlayback();
}
function verifyPlayback(){
setTimeout(()=>{
const v = document.querySelector('video');
if(v && !v.paused && v.currentTime > 0) return;
if(v && v.paused && v.readyState >= 2 && config.autoPlay){
v.play().catch(()=>{});
}
if(!v){
const cur = findCurrentEntry();
if(cur){
doClick(cur);
}
}
}, 6000);
}
function installClickTracker(){
document.addEventListener('click', (e)=>{
const item = e.target.closest('.resource-item');
if(item){
lastUserClickTime = Date.now();
const all = getAllEntries();
const i = all.indexOf(item);
if(i>=0){
globalIdx = i;
curEntry = item;
}
}
}, true);
}
// ============ 进度保存/统计 ============
function saveProgress(){
if(globalIdx>=0){
try{ localStorage.setItem('smartedu_progress', JSON.stringify({idx:globalIdx})); }catch(e){}
}
}
function loadProgress(){
try{
const d = JSON.parse(localStorage.getItem('smartedu_progress')||'null');
if(d && d.idx>=0) return d.idx;
}catch(e){}
return -1;
}
function getPlayedSet(){
try{ return new Set(JSON.parse(localStorage.getItem('smartedu_played')||'[]')); }catch(e){ return new Set(); }
}
function markPlayed(entry){
if(!entry) return;
try{
const s = getPlayedSet();
s.add((entry.textContent||'').trim());
localStorage.setItem('smartedu_played', JSON.stringify(Array.from(s)));
}catch(e){}
}
function collectCourses(){
const all = getAllEntries();
let done = 0, todo = 0;
all.forEach(el=>{ if(isDone(el)) done++; else todo++; });
return {done, todo, total: all.length};
}
function refreshNowLine(){
const nowLine = document.getElementById('nowLine');
const nextLine = document.getElementById('nextLine');
if(!nowLine) return;
const cur = findCurrentEntry();
if(cur){
nowLine.textContent = '▶ 正在: '+(cur.textContent||'').replace(/\s+/g,' ').trim().slice(0,22);
const n = findNextLesson();
nextLine.textContent = n.next ? '⏭ 下一节: '+(n.next.textContent||'').replace(/\s+/g,' ').trim().slice(0,22) : '⏭ 下一节: '+n.reason;
}else{
nowLine.textContent = '▶ 正在: 未定位(先手动点开一课)';
nextLine.textContent = '⏭ 下一节: -';
}
}
function updateStat(feedback){
if(!config.showStat) return;
const statLine = document.getElementById('statLine');
if(!statLine) return;
const s = collectCourses();
const pct = s.total ? Math.round(s.done/s.total*100) : 0;
document.getElementById('progBar').style.width = pct+'%';
statLine.textContent = `已学${s.done} · 未学${s.todo} · 总${s.total} (${pct}%)`;
refreshNowLine();
const fbLine = document.getElementById('feedbackLine');
if(feedback && fbLine){
fbLine.innerHTML = feedback;
fbLine.style.animation = 'none';
fbLine.offsetHeight;
fbLine.style.animation = 'fbFlash 1s';
if(!document.getElementById('fbFlashStyle')){
const st = document.createElement('style');
st.id = 'fbFlashStyle';
st.textContent = '@keyframes fbFlash{0%{background:#fff3cd;}100%{background:transparent;}}';
document.head.appendChild(st);
}
}
}
function diagCourses(){
const all = getAllEntries();
const lines = [];
lines.push('===== A. 全局顺序条目链(真实DOM顺序) =====');
let lastPath = '';
all.forEach((el,i)=>{
const path = getModulePath(el);
const t = (el.textContent||'').replace(/\s+/g,' ').trim().slice(0,42);
const st = getItemState(el);
const mark = st==='done' ? '✓' : (st==='processing' ? '🔄' : '○');
if(path && path !== lastPath){
lines.push(`—— [${path}]`);
lastPath = path;
}
lines.push(`${i+1}.${mark} ${t}`);
});
lines.push(`共 ${all.length} 节`);
lines.push('');
lines.push('===== B. 定位信息 =====');
const cur = findCurrentEntry();
lines.push('当前: ' + (cur ? (cur.textContent||'').replace(/\s+/g,' ').trim().slice(0,40) : '未定位'));
lines.push('记忆索引: ' + (globalIdx>=0 ? '第'+(globalIdx+1)+'节' : '无'));
const first = findFirstUnfinished(0);
lines.push('第一个未完成(从最上面): ' + (first>=0 ? '第'+(first+1)+'节 '+(all[first].textContent||'').replace(/\s+/g,' ').trim().slice(0,30) : '无(已全部刷完)'));
return lines.slice(0,400).join('\n');
}
// ============ 视频倍速伪装 ============
function getSpeedDesc(obj){
while(obj && obj !== Object.prototype){
const d = Object.getOwnPropertyDescriptor(obj, 'playbackRate');
if(d) return d;
obj = Object.getPrototypeOf(obj);
}
return null;
}
function hijackSpeed(video){
if(!video || video.__hijacked) return;
video.__hijacked = true;
const desc = getSpeedDesc(video);
if(desc && desc.set){
Object.defineProperty(video, 'playbackRate', {
configurable: true,
get(){ return 1; },
set(){ desc.set.call(this, config.speed); }
});
desc.set.call(video, config.speed);
}
if(config.autoMute) video.muted = true;
}
function applySpeed(video){
if(!video) return;
const desc = getSpeedDesc(video);
if(desc && desc.set) desc.set.call(video, config.speed);
}
function tryAutoPlay(){
if(!config.autoPlay) return;
const video = currentVideo || document.querySelector('video');
if(!video) return;
if(video.paused && !video.ended && video.readyState >= 2){
video.play().catch(()=>{});
}
}
function keepPageAlive() {
if(!config.keepAlive) return;
Object.defineProperty(document, 'visibilityState', { value: 'visible', writable: false });
Object.defineProperty(document, 'hidden', { value: false, writable: false });
document.dispatchEvent(new Event('visibilitychange'));
if(currentVideo && currentVideo.paused) tryAutoPlay();
}
function expandAllChapters(){
document.querySelectorAll('.fish-collapse-item').forEach(item=>{
if(item.classList.contains('fish-collapse-item-active')) return;
const h = item.querySelector(':scope > .fish-collapse-header');
if(h && !h.__expanded){
h.__expanded = true;
h.click();
}
});
document.querySelectorAll('[class*="expand"][aria-expanded="false"],[class*="unfold"],[class*="more"][class*="btn"]').forEach(el=>{
if(!el.__expanded){
el.__expanded = true;
el.click();
}
});
}
function scrollCatalogContainer(){
document.querySelectorAll('[class*="catalog"],[class*="menu"],[class*="sidebar"],[class*="tree"],[class*="list"],[class*="directory"],[class*="wrap"]').forEach(c=>{
if(c.scrollHeight > c.clientHeight + 100){
c.scrollTop = c.scrollHeight;
}
});
window.scrollTo(0, document.body.scrollHeight);
window.dispatchEvent(new WheelEvent('wheel', {deltaY: 1, deltaMode: 0}));
}
function autoScrollPage() {
if (!config.autoScroll || scrollRan) return;
scrollRan = true;
expandAllChapters();
let lastCount = -1;
let stableCount = 0;
let timer = setInterval(()=>{
expandAllChapters();
scrollCatalogContainer();
const count = getAllEntries().length;
if(count === lastCount){
stableCount++;
} else {
stableCount = 0;
lastCount = count;
}
if(stableCount >= 6){
clearInterval(timer);
window.scrollTo({top: 0, behavior: 'auto'});
updateStat();
}
}, 800);
setTimeout(()=>{
clearInterval(timer);
window.scrollTo({top: 0, behavior: 'auto'});
updateStat();
}, 40000);
}
function closeDetectModal(){
if(!config.closeModal) return;
const keywords = /倍速|暂停学习|学习已暂停|关闭倍速播放|检测到|学时|须学习完|学习完课程|完整观看|未学习完|看完视频/i;
const containers = [
'.el-message-box','.el-dialog','.el-notification','.ant-modal',
'.modal','.popup','.toast',
'[class*="fish-message"]','[class*="fish-dialog"]','[class*="fish-modal"]',
'[class*="message-box"]','[class*="dialog"]'
];
document.querySelectorAll(containers.join(',')).forEach(el=>{
if(!el.innerText || !keywords.test(el.innerText)) return;
const btns = el.querySelectorAll('button');
let hit = null;
btns.forEach(b=>{
if(/知道|确定|关闭|OK|好的|取消/.test((b.textContent||'').trim())) hit = b;
});
if(hit){ hit.click(); return; }
const closeBtn = el.querySelector('.el-dialog__headerbtn,.el-message-box__close,.close,[aria-label="关闭"],[class*="headerbtn"],[class*="close-btn"],[class*="modal-close"]');
if(closeBtn){ closeBtn.click(); return; }
el.style.display='none';
el.remove();
});
}
// ============ 自动切下一节 ============
function autoNextVideo() {
if (!config.autoNext) return;
const video = currentVideo;
if(!video) return;
if(bindEnded) return;
bindEnded = true;
video.addEventListener('ended', videoEnd);
}
function watchForEnd(){
const v = currentVideo;
if(!v || !v.duration || v.duration === Infinity) return;
if(v.ended || (v.currentTime >= v.duration - 1)){
if(!v.__endFired){
v.__endFired = true;
setTimeout(videoEnd, 300);
}
}
}
function videoEnd(){
if(Date.now() - lastSwitchTime < 3000) return;
lastSwitchTime = Date.now();
if(!config.autoNext) return;
markPlayed(findCurrentEntry());
saveProgress();
const r = findNextLesson();
if(r.next){
clickEntry(r.next);
globalIdx = r.idx;
scrollRan = false;
bindEnded = false;
currentVideo = null;
const s = collectCourses();
updateStat(`✅ 刷完一节!已学 ${s.done}/${s.total}(${Math.round(s.done/s.total*100)}%),继续~`);
setTimeout(()=>{
ensureVideo();
autoScrollPage();
tryAutoPlay();
updateStat();
},1800);
} else {
updateStat('🏁 ' + r.reason);
}
}
// ============ 跳过已学完 ============
function skipDoneLesson(){
if(!config.skipDone) return;
const cur = findCurrentEntry();
if(!cur) return;
if(isUnfinished(cur)) return;
const total = collectCourses().total || 50;
if(skipCount >= total + 5){
updateStat('🏁 检测到已全部刷完,停止跳转');
return;
}
skipCount++;
const r = findNextLesson();
if(r.next){
markPlayed(cur);
saveProgress();
clickEntry(r.next);
globalIdx = r.idx;
scrollRan = false;
bindEnded = false;
currentVideo = null;
updateStat('⏭️ 已学完,跳到下一个未完成: 第'+(r.idx+1)+'节');
setTimeout(()=>{
ensureVideo();
autoScrollPage();
tryAutoPlay();
updateStat();
},1500);
} else {
updateStat('🏁 ' + r.reason);
}
}
function ensureVideo(){
const video = document.querySelector('video');
if(!video) return;
if(video !== currentVideo){
currentVideo = video;
video.__endFired = false;
hijackSpeed(video);
autoNextVideo();
setTimeout(skipDoneLesson, 3000);
}
}
// ============ 启动:强制从第一个未完成开始 ============
function startSmart(){
if(!config.autoStart) return;
let attempts = 0;
function tryStart(){
attempts++;
const all = getAllEntries();
if(!all.length || all.length < 30){
if(attempts < 25){ setTimeout(tryStart, 1000); return; }
}
const firstIdx = findFirstUnfinished(0);
if(firstIdx < 0){ updateStat('🏁 全部课程已刷完!'); return; }
if(Date.now() - lastUserClickTime < 30000) return;
const cur = findCurrentEntry();
const curIdx = cur ? all.indexOf(cur) : -1;
if(curIdx === firstIdx){
updateStat('▶ 已在第一个未完成: 第'+(firstIdx+1)+'节');
return;
}
clickEntry(all[firstIdx]);
globalIdx = firstIdx;
ensureEntryVisible(all[firstIdx]);
autoScrollPage();
tryAutoPlay();
updateStat('🎯 已从最上面定位到第一个未完成: 第'+(firstIdx+1)+'节 '+(all[firstIdx].textContent||'').replace(/\s+/g,' ').trim().slice(0,24));
}
setTimeout(tryStart, 4000);
}
function watchCatalog(){
if(observer) return;
const target = document.querySelector('.fish-collapse') || document.body;
observer = new MutationObserver(()=>{
clearTimeout(observer.__t);
observer.__t = setTimeout(()=>{
expandAllChapters();
updateStat();
refreshNowLine();
}, 600);
});
observer.observe(target, {childList:true, subtree:true});
}
function init(){
if(!document.getElementById("studyPanel")) createPanel();
installClickTracker();
ensureVideo();
autoScrollPage();
tryAutoPlay();
closeDetectModal();
updateStat();
watchCatalog();
startSmart();
}
window.addEventListener('load', init);
setInterval(()=>{
ensureVideo();
tryAutoPlay();
watchForEnd();
keepPageAlive();
closeDetectModal();
refreshNowLine();
updateStat();
}, 1000);
})();