// ==UserScript== // @name 广西干部网络学院 学习助手(自动刷课+状态总览) // @namespace https://www.gxela.gov.cn/ // @version 8.0.1 // @description 广西干部网络学院平台学习辅助(个人测试用途,禁止倒卖)。一个脚本含两个面板:播放页自动刷课+全站学习状态总览。 // @author YG // @match https://www.gxela.gov.cn/* // @run-at document-idle // @grant none // @license MIT // @antifeature none // ==/UserScript== (function () { 'use strict'; if (window.__GXELA_HELPER__) return; window.__GXELA_HELPER__ = true; /* ============================================================ 公共配置 ============================================================ */ var CFG = { enabled: true, mute: true, autoNext: true, skipDone: true, skipDelayMs: 2500, tickMs: 3000, stallReloadMs: 240000 }; try { var _saved = JSON.parse(localStorage.getItem('__gxelaCfg') || '{}'); for (var _k in _saved) if (_saved.hasOwnProperty(_k)) CFG[_k] = _saved[_k]; } catch (e) {} function saveCfg() { try { localStorage.setItem('__gxelaCfg', JSON.stringify({ enabled: CFG.enabled, mute: CFG.mute, autoNext: CFG.autoNext, skipDone: CFG.skipDone })); } catch (e) {} } var logs = []; function log(m) { var t = new Date().toTimeString().slice(0, 8); logs.push('[' + t + '] ' + m); if (logs.length > 60) logs.shift(); try { console.log('[网院] ' + m); } catch (e) {} } function esc(v) { return String(v == null ? '' : v) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, '''); } /* ============================================================ 接口层 ============================================================ */ function token() { var m = document.cookie.match(/GXELA-Token=([^;]+)/); return m ? decodeURIComponent(m[1]) : ''; } function headers() { return { 'Authorization': 'Bearer ' + token(), 'Clientid': 'e5cd7e4891bf95d1d19206ce24a7b32e', 'platform': 'web', 'Content-Type': 'application/json;charset=utf-8' }; } function apiGet(path) { return fetch('/gateway' + path, { headers: headers(), credentials: 'include' }) .then(function (r) { return r.json(); }); } function apiPost(path, body) { return fetch('/gateway' + path, { method: 'POST', headers: headers(), body: JSON.stringify(body), credentials: 'include' }).then(function (r) { return r.json(); }); } function lessonIdFromUrl() { var m = location.search.match(/lessonId=(\d+)/); return m ? m[1] : null; } function isLessonDone(r) { if (!r) return false; var p = parseFloat(r.learnProgress || 0); if (p >= 100) return true; if (r.passed === 1 || r.passed === '1' || r.passed === true) return true; if (r.userEvtStatus === 1) return true; return false; } /* ============================================================ 必修课队列(两个面板共用同一份数据) ============================================================ */ var QUEUE = { groups: [], built: false, building: false, tcId: '', classes: [], lastError: '' }; var TC_KEY = '__gxelaTcId'; function pickTrainClass(classes) { if (!classes.length) return null; var saved = ''; try { saved = localStorage.getItem(TC_KEY) || ''; } catch (e) {} if (saved) { for (var i = 0; i < classes.length; i++) { if (String(classes[i].trainClassId) === String(saved)) return classes[i]; } } var y = String(new Date().getFullYear()); for (var k = 0; k < classes.length; k++) { if (String(classes[k].name || '').indexOf(y) >= 0) return classes[k]; } return classes[0]; } function fetchTrainClass(forceId) { return apiGet('/auth/trainclass/myTrainclasspage').then(function (j) { var list = (j && j.rows) || []; QUEUE.classes = list; var cls = null; if (forceId) { for (var i = 0; i < list.length; i++) { if (String(list[i].trainClassId) === String(forceId)) { cls = list[i]; break; } } } if (!cls) cls = pickTrainClass(list); if (!list.length) { if (!token()) throw new Error('未登录,请先登录广西干部网络学院'); throw new Error('没有可用班级,请确认已加入班级'); } if (!cls) throw new Error('没有可用班级'); QUEUE.tcId = String(cls.trainClassId); try { localStorage.setItem(TC_KEY, QUEUE.tcId); } catch (e) {} return QUEUE.tcId; }); } function fetchCats(tcId) { return apiGet('/auth/trainclass/top/' + tcId).then(function (j) { var arr = (j && j.data) || []; arr.sort(function (a, b) { return (a.sort || 0) - (b.sort || 0); }); return arr; }); } function fetchLessonsByCat(tcId, catId) { var all = [], page = 1; function step() { return apiPost('/auth/trainclass/category/lessonpage', { trainClassId: tcId, isMust: 1, pageNum: page, pageSize: 100, categoryIds: [catId] }).then(function (j) { var rows = (j && j.rows) || []; var total = (j && j.total) || 0; all = all.concat(rows); if (rows.length >= 100 && all.length < total) { page++; return step(); } return all; }); } return step(); } function buildQueue(classId) { if (QUEUE.building) return Promise.resolve(QUEUE.groups); QUEUE.building = true; log('正在构建必修课队列…'); var tc = (classId || QUEUE.tcId) ? Promise.resolve(classId || QUEUE.tcId) : fetchTrainClass(); return tc.then(function (tcId) { if (classId) QUEUE.tcId = String(classId); return fetchCats(tcId).then(function (cats) { if (!cats.length) throw new Error('该班级无必修课分类'); var jobs = cats.map(function (c) { var cid = String(c.categoryId); return fetchLessonsByCat(tcId, cid).then(function (rows) { return { id: cid, name: c.name, lessons: rows }; }).catch(function () { return { id: cid, name: c.name, lessons: [] }; }); }); return Promise.all(jobs); }); }).then(function (groups) { QUEUE.groups = groups; QUEUE.built = true; QUEUE.building = false; QUEUE.lastError = ''; var tot = 0, done = 0; groups.forEach(function (g) { var dn = g.lessons.filter(isLessonDone).length; tot += g.lessons.length; done += dn; log(' ' + g.name + ': ' + dn + '/' + g.lessons.length + ' 剩' + (g.lessons.length - dn)); }); log('队列就绪:共 ' + tot + ' 节,已完成 ' + done + ',剩 ' + (tot - done)); return groups; }).catch(function (e) { QUEUE.building = false; QUEUE.lastError = e && e.message ? e.message : String(e); log('构建队列失败: ' + QUEUE.lastError); return []; }); } function queueRowOf(id) { for (var i = 0; i < QUEUE.groups.length; i++) { var ls = QUEUE.groups[i].lessons; for (var j = 0; j < ls.length; j++) { if (String(ls[j].lessonId) === String(id)) return ls[j]; } } return null; } function pickNextFromQueue() { var cur = lessonIdFromUrl(); var flat = []; QUEUE.groups.forEach(function (g) { g.lessons.forEach(function (r) { flat.push({ row: r, gname: g.name }); }); }); if (!flat.length) return null; var idx = -1; for (var i = 0; i < flat.length; i++) { if (String(flat[i].row.lessonId) === String(cur)) { idx = i; break; } } var seq = []; if (idx >= 0) { for (var a = idx + 1; a < flat.length; a++) seq.push(flat[a]); for (var b = 0; b < idx; b++) seq.push(flat[b]); } else { seq = flat; } for (var k = 0; k < seq.length; k++) { if (!isLessonDone(seq[k].row)) return seq[k]; } return null; } /* ============================================================ 播放控制 ============================================================ */ var video = null; var lastT = -1, lastChange = 0, lastReload = 0; var endHandled = false; var curLessonId = null, curGkey = null, lessonInfo = null; var saveTimer = null; var skipChecked = false, skipping = false, nextLock = false; function bindVideo() { var v = document.querySelector('video'); if (!v || v === video) return; video = v; endHandled = false; lastT = -1; lastChange = Date.now(); log('已接管视频,时长 ' + Math.floor(v.duration || 0) + ' 秒'); v.addEventListener('loadedmetadata', onMeta); v.addEventListener('durationchange', onMeta); v.addEventListener('play', onMeta); v.addEventListener('pause', function () { if (v.ended) return; setTimeout(function () { if (v.paused && !v.ended) play(); }, 800); }); v.addEventListener('ended', onEnded); onMeta(); } function onMeta() { if (!video) return; if (!isFinite(video.duration) || video.duration < 10) return; if (CFG.mute) video.muted = true; if (video.paused) play(); } function play() { if (!video || !CFG.enabled) return; video.muted = CFG.mute; try { var p = video.play(); if (p && p.catch) p.catch(function (e) { log('播放受阻: ' + (e && e.name)); }); } catch (e) {} } function loadLessonInfo(cb) { var id = lessonIdFromUrl(); if (!id) { cb && cb(null); return; } if (curLessonId === id && lessonInfo) { cb && cb(lessonInfo); return; } var q = queueRowOf(id); if (q) { lessonInfo = q; curLessonId = id; curGkey = q.gkey || ''; log('课程: ' + (q.name || '').slice(0, 20) + ' 进度 ' + (q.learnProgress || 0) + '%'); if (!curGkey) fetchGkey(id); checkIfDone(); cb && cb(lessonInfo); return; } apiGet('/auth/course/lesson/' + id).then(function (j) { if (j && j.data) { lessonInfo = j.data; curLessonId = id; curGkey = j.data.gkey; log('课程: ' + (j.data.name || '').slice(0, 20) + ' 进度 ' + j.data.learnProgress + '%'); checkIfDone(); } cb && cb(lessonInfo); }).catch(function () { cb && cb(null); }); } function fetchGkey(id) { apiGet('/auth/course/lesson/' + id).then(function (j) { if (j && j.data && j.data.gkey) { curGkey = j.data.gkey; } }).catch(function () {}); } function checkIfDone() { if (skipChecked) return; skipChecked = true; if (!CFG.skipDone || !CFG.autoNext || !CFG.enabled) return; if (!isLessonDone(lessonInfo)) return; var name = (lessonInfo.name || '').slice(0, 20); log('《' + name + '》已完成,' + (CFG.skipDelayMs / 1000) + ' 秒后跳下一门'); skipping = true; try { if (video) video.pause(); } catch (e) {} setTimeout(function () { goNextLesson(true); }, CFG.skipDelayMs); } function startSaveLoop() { if (saveTimer) return; saveTimer = setInterval(function () { if (!CFG.enabled || !video || !curLessonId || !curGkey) return; if (!isFinite(video.duration) || video.duration < 10) return; if (video.paused) return; var pct = +(video.currentTime / video.duration * 100).toFixed(2); var tcLid = (lessonInfo && lessonInfo.tcLessonId != null) ? lessonInfo.tcLessonId : 0; apiPost('/auth/user/study/progress/save', { lessonId: curLessonId, lessonGkey: curGkey, lessonOrigin: 'trainclass', tcLessonId: String(tcLid), learnTime: Math.floor(video.currentTime), progress: pct, thisTimeLearnSeconds: 0 }).then(function (j) { if (j && j.code === 200) log('进度已上报 ' + pct + '%'); }).catch(function () {}); }, 60000); } function onEnded() { if (endHandled) return; endHandled = true; log('本课播放结束'); if (!CFG.autoNext) return; setTimeout(function () { goNextLesson(); }, 4000); } function goNextLesson(manual) { if (skipping && !manual) return; if (nextLock) return; nextLock = true; setTimeout(function () { nextLock = false; }, 8000); var proceed = QUEUE.built ? Promise.resolve(QUEUE.groups) : buildQueue(); proceed.then(function () { var hit = pickNextFromQueue(); if (!hit) { log('★ 必修课全部完成,没有待学课程了'); skipping = false; refreshStatusPanels(); return; } var cur = lessonIdFromUrl(); var curGroup = null; QUEUE.groups.forEach(function (g) { if (g.lessons.some(function (r) { return String(r.lessonId) === String(cur); })) curGroup = g.name; }); if (curGroup && curGroup !== hit.gname) { log('跨大课推进:' + curGroup + ' → ' + hit.gname); } log('下一节 [' + hit.gname + '] ' + (hit.row.name || '').slice(0, 22)); var tcLid = (hit.row.tcLessonId != null) ? hit.row.tcLessonId : 0; location.href = '/watch?lessonId=' + hit.row.lessonId + '&tcLessonId=' + tcLid + '&lessonOrigin=trainclass'; }).catch(function (e) { log('切换失败: ' + e.message); skipping = false; }); } function dismissPopup() { var texts = ['确定', '确认', '我知道了', '知道了', '继续学习', '继续观看', '继续播放', '好的']; var pops = document.querySelectorAll('.el-dialog, .el-message-box, .el-message, [role="dialog"]'); for (var i = 0; i < pops.length; i++) { var el = pops[i]; if (!el.getBoundingClientRect().width) continue; var btns = el.querySelectorAll('button, .el-button'); for (var j = 0; j < btns.length; j++) { var t = (btns[j].innerText || '').replace(/\s+/g, ''); if (texts.indexOf(t) >= 0) { btns[j].click(); log('弹窗已确认: ' + t); return; } } } } /* ============================================================ 通用面板工具 ============================================================ */ function clamp(v, a, b) { return Math.max(a, Math.min(a > b ? a : b, v)); } function makeDraggable(host, handle, posKey, getBox) { var st = null; handle.addEventListener('pointerdown', function (e) { if (e.target && e.target.closest && e.target.closest('.ic, .x, button, select')) return; if (e.button !== 0 && e.button !== undefined) return; var r = host.getBoundingClientRect(); st = { px: e.clientX, py: e.clientY, left: r.left, top: r.top, moved: false }; try { handle.setPointerCapture(e.pointerId); } catch (err) {} e.preventDefault(); }); handle.addEventListener('pointermove', function (e) { if (!st) return; var dx = e.clientX - st.px, dy = e.clientY - st.py; if (!st.moved && Math.abs(dx) + Math.abs(dy) < 3) return; st.moved = true; var box = getBox(); var w = host.offsetWidth || 270, h = host.offsetHeight || 100; host.style.left = clamp(st.left + dx, 4, Math.max(4, (window.innerWidth || 800) - w - 4)) + 'px'; host.style.top = clamp(st.top + dy, 4, Math.max(4, (window.innerHeight || 600) - h - 4)) + 'px'; }); function end(e) { if (!st) return; if (st.moved) { var r = host.getBoundingClientRect(); try { localStorage.setItem(posKey, JSON.stringify({ left: Math.round(r.left), top: Math.round(r.top) })); } catch (err) {} } try { handle.releasePointerCapture(e.pointerId); } catch (err) {} st = null; } handle.addEventListener('pointerup', end); handle.addEventListener('pointercancel', end); } function restorePos(host, posKey) { try { var p = JSON.parse(localStorage.getItem(posKey) || 'null'); if (p && typeof p.left === 'number' && typeof p.top === 'number') { host.style.left = p.left + 'px'; host.style.top = p.top + 'px'; host.style.right = 'auto'; } } catch (e) {} } function fillWatermark(root, cols, rows, stepX, stepY) { var wm = root.querySelector('.wm'); if (!wm) return; var html = ''; for (var r = 0; r < rows; r++) { for (var c = 0; c < cols; c++) { html += '麦乐坤'; } } wm.innerHTML = html; } /* ============================================================ 共享样式(两个面板共用) ============================================================ */ var SHARED_CSS = [ /* 毛玻璃基底 */ '.p{font:12px/1.6 "Microsoft YaHei",-apple-system,"Segoe UI",sans-serif;color:#f2f7ff;', ' position:relative;width:268px;height:420px;display:flex;flex-direction:column;', ' resize:none;overflow:hidden;border-radius:22px;', ' background:linear-gradient(160deg,rgba(38,46,72,.82) 0%,rgba(26,32,54,.88) 45%,rgba(32,38,62,.84) 100%);', ' backdrop-filter:blur(32px) saturate(190%);-webkit-backdrop-filter:blur(32px) saturate(190%);', ' border:1px solid rgba(255,255,255,.32);', ' box-shadow:0 22px 54px -12px rgba(0,0,0,.6),0 8px 24px rgba(0,0,0,.3),', ' inset 0 1px 0 rgba(255,255,255,.55),inset 0 -1px 0 rgba(255,255,255,.10),', ' inset 1px 0 0 rgba(255,255,255,.14),inset -1px 0 0 rgba(255,255,255,.14)}', '.p::before{content:"";position:absolute;left:0;right:0;top:0;height:46%;pointer-events:none;z-index:1;', ' border-radius:22px 22px 50% 50%/22px 22px 22px 22px;', ' background:linear-gradient(180deg,rgba(255,255,255,.16) 0%,rgba(255,255,255,.04) 52%,rgba(255,255,255,0) 100%)}', '.p::after{content:"";position:absolute;left:0;right:0;bottom:0;height:32%;pointer-events:none;z-index:1;', ' border-radius:50% 50% 22px 22px/22px 22px 22px 22px;', ' background:linear-gradient(0deg,rgba(120,160,255,.10) 0%,rgba(120,160,255,0) 100%)}', /* 水印 */ '.wm{position:absolute;inset:0;pointer-events:none;z-index:0;overflow:hidden;border-radius:22px}', '.wm span{position:absolute;font:600 13px/1 "Microsoft YaHei",sans-serif;color:rgba(150,190,255,.10);', ' white-space:nowrap;transform:rotate(-22deg);letter-spacing:3px;', ' text-shadow:0 0 12px rgba(130,175,255,.18)}', /* 标题栏 */ '.h{position:relative;z-index:3;display:flex;align-items:center;gap:9px;padding:11px 14px;', ' border-radius:22px 22px 0 0;font-weight:600;', ' background:linear-gradient(180deg,rgba(255,255,255,.16),rgba(255,255,255,.02));', ' border-bottom:1px solid rgba(255,255,255,.14);', ' cursor:move;user-select:none;-webkit-user-select:none;touch-action:none;flex:0 0 auto}', '.h:active{background:linear-gradient(180deg,rgba(255,255,255,.24),rgba(255,255,255,.06))}', '.h b{flex:1;font-size:12.5px;letter-spacing:1px;', ' background:linear-gradient(92deg,#ffffff,#b9d4ff);-webkit-background-clip:text;', ' background-clip:text;-webkit-text-fill-color:transparent}', '.ic{cursor:pointer;color:rgba(240,248,255,.72);padding:3px 7px;font-size:14px;line-height:1;', ' border-radius:999px;transition:background .2s,color .2s,transform .2s cubic-bezier(.34,1.56,.64,1)}', '.ic:hover{background:rgba(255,255,255,.2);color:#fff;transform:scale(1.18)}', '.ic:active{transform:scale(.92)}', /* 按钮:胶囊 + 液态放大 */ 'button{font:11px/1 inherit;padding:8px 10px;border-radius:999px;cursor:pointer;color:#f2f7ff;', ' letter-spacing:.4px;transform:translateZ(0);will-change:transform,box-shadow;', ' border:1px solid rgba(255,255,255,.26);', ' background:linear-gradient(180deg,rgba(255,255,255,.20),rgba(255,255,255,.06));', ' backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);', ' box-shadow:0 1px 0 rgba(255,255,255,.30) inset,0 4px 12px rgba(0,0,0,.18);', ' transition:background .22s,transform .22s cubic-bezier(.34,1.56,.64,1),box-shadow .22s}', 'button:hover{background:linear-gradient(180deg,rgba(255,255,255,.32),rgba(255,255,255,.14));', ' transform:scale(1.08) translateY(-1px);', ' box-shadow:0 10px 26px rgba(120,170,255,.5),0 1px 0 rgba(255,255,255,.45) inset}', 'button:active{transform:scale(.95) translateY(0)}', 'button.on{background:linear-gradient(180deg,rgba(110,170,255,.95),rgba(60,115,235,.95));', ' border-color:rgba(170,210,255,.75);color:#fff;', ' box-shadow:0 0 20px rgba(110,170,255,.6),0 1px 0 rgba(255,255,255,.45) inset}', /* 滚动条 */ '*::-webkit-scrollbar{width:5px}', '*::-webkit-scrollbar-track{background:transparent}', '*::-webkit-scrollbar-thumb{background:rgba(255,255,255,.22);border-radius:999px}', '*::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.36)}' ].join('\n'); /* 状态颜色 */ var COLOR_CSS = [ '.ok{color:#63eda6;text-shadow:0 0 9px rgba(99,237,166,.5)}', '.mid{color:#ffd470;text-shadow:0 0 9px rgba(255,212,112,.45)}', '.no{color:#ff96a3;text-shadow:0 0 9px rgba(255,150,163,.45)}' ].join('\n'); /* ============================================================ 面板 A:自动学习(播放页) ============================================================ */ var panelA = null; function buildLearnPanel() { if (panelA) return; if (!location.pathname.startsWith('/watch') && !location.pathname.startsWith('/lesson')) return; var host = document.createElement('div'); host.style.cssText = 'position:fixed;top:72px;right:12px;z-index:2147483646'; var root = host.attachShadow ? host.attachShadow({ mode: 'open' }) : host; root.innerHTML = [ '', '
', '
网院自动学习–
', '
', '
状态待机
', '
进度--
', '
下一门--
', '
所在大课--
', '
大课进度--
', '
', '
', '
', '
' ].join('\n'); (document.body || document.documentElement).appendChild(host); restorePos(host, '__gxelaPanelPos'); makeDraggable(host, root.querySelector('[data-drag]'), '__gxelaPanelPos', function () { return root.querySelector('.p'); }); fillWatermark(root, 3, 10, 86, 54); root.addEventListener('click', function (ev) { var t = ev.target.closest && ev.target.closest('[data-a]'); if (!t) return; var a = t.getAttribute('data-a'); if (a === 'min') { root.querySelector('.p').classList.toggle('open'); return; } if (a === 'tg') { CFG.enabled = !CFG.enabled; saveCfg(); if (CFG.enabled) onMeta(); log(CFG.enabled ? '已开启' : '已暂停'); return; } if (a === 'nx2') { curLessonId = null; lessonInfo = null; skipping = false; goNextLesson(true); return; } if (a === 'qb') { QUEUE.built = false; QUEUE.groups = []; log('队列已重置,重新构建…'); buildQueue().then(function () { log('队列已刷新'); }); return; } if (a === 'sk') { CFG.skipDone = !CFG.skipDone; saveCfg(); log(CFG.skipDone ? '已开启:跳过已完成' : '已关闭:跳过已完成'); if (CFG.skipDone) { skipChecked = false; checkIfDone(); } return; } }, true); panelA = { host: host, root: root }; } function updateLearnPanel() { if (!panelA) return; var q = function (k) { return panelA.root.querySelector('[data-k="' + k + '"]'); }; var done = isLessonDone(lessonInfo); var st = '等待视频'; if (!CFG.enabled) st = '已暂停'; else if (skipping) st = '已完成·跳过中'; else if (done && CFG.skipDone) st = '已完成'; else if (!video) st = '等待视频'; else if (video.ended) st = '本课结束'; else if (video.paused) st = '恢复中'; else st = '学习中'; var e = q('st'); if (e) e.textContent = st; e = q('pg'); if (e) { if (done && lessonInfo) e.textContent = '100%(已完成)'; else e.textContent = (video && isFinite(video.duration) && video.duration > 0) ? Math.floor(video.currentTime / video.duration * 100) + '%' : '--'; } e = q('nx'); if (e) e.textContent = lessonInfo ? (lessonInfo.name || '').slice(0, 14) : '--'; var grpName = '--', grpPct = '--'; if (QUEUE.built && QUEUE.groups.length) { var cur = lessonIdFromUrl(); for (var gi = 0; gi < QUEUE.groups.length; gi++) { var g = QUEUE.groups[gi]; var found = false; for (var li = 0; li < g.lessons.length; li++) { if (String(g.lessons[li].lessonId) === String(cur)) { found = true; break; } } if (found) { var dn = g.lessons.filter(isLessonDone).length; var tt = g.lessons.length; grpName = g.name; grpPct = dn + '/' + tt + (dn >= tt ? ' ✓' : ' 剩' + (tt - dn)); break; } } } e = q('grp'); if (e) e.textContent = grpName; e = q('gpg'); if (e) e.textContent = grpPct; e = q('lg'); if (e) e.textContent = logs.slice(-6).join('\n'); var d = panelA.root.querySelector('.d'); if (d) d.className = CFG.enabled ? 'd' : 'd off'; var b = panelA.root.querySelector('[data-a="tg"]'); if (b) { b.textContent = CFG.enabled ? '暂停' : '开启'; b.className = CFG.enabled ? 'on' : ''; } var sk = panelA.root.querySelector('[data-a="sk"]'); if (sk) { sk.textContent = CFG.skipDone ? '跳过已完成 ✓' : '跳过已完成 ✗'; sk.className = CFG.skipDone ? 'on' : ''; } } /* ============================================================ 面板 B:学习状态(全站) ============================================================ */ var panelB = null; var OPT = { onlyUndone: false, expanded: {} }; try { var _opt = JSON.parse(localStorage.getItem('__gxelaStatusOpt') || '{}'); for (var _ok in _opt) if (_opt.hasOwnProperty(_ok)) OPT[_ok] = _opt[_ok]; } catch (e) {} function saveOpt() { try { localStorage.setItem('__gxelaStatusOpt', JSON.stringify(OPT)); } catch (e) {} } function buildStatusPanel() { if (panelB) return; var host = document.createElement('div'); host.style.cssText = 'position:fixed;left:12px;top:12px;z-index:2147483646'; var root = host.attachShadow ? host.attachShadow({ mode: 'open' }) : host; root.innerHTML = [ '', '
', '
必修课学习状态–×
', '
', ' ', ' ', ' ', '
', '
正在读取…
', '
', '
' ].join('\n'); (document.body || document.documentElement).appendChild(host); restorePos(host, '__gxelaStatusPos'); makeDraggable(host, root.querySelector('[data-drag]'), '__gxelaStatusPos', function () { return root.querySelector('.p'); }); fillWatermark(root, 4, 12, 86, 54); root.querySelector('.bar').addEventListener('click', function (ev) { var t = ev.target.closest && ev.target.closest('[data-a]'); if (!t) return; var a = t.getAttribute('data-a'); if (a === 'refresh') { refreshStatus(); return; } if (a === 'undone') { OPT.onlyUndone = !OPT.onlyUndone; saveOpt(); renderStatus(); return; } }); root.querySelector('.bar').addEventListener('change', function (ev) { var t = ev.target.closest && ev.target.closest('[data-a="cls"]'); if (!t) return; var v = t.value; QUEUE.built = false; QUEUE.groups = []; buildQueue(v).then(function () { renderStatus(); }); }); root.querySelector('.h').addEventListener('click', function (ev) { var t = ev.target.closest && ev.target.closest('[data-a]'); if (!t) return; var a = t.getAttribute('data-a'); if (a === 'min') { root.querySelector('.p').classList.toggle('mini'); return; } if (a === 'close') { host.remove(); panelB = null; return; } }); root.querySelector('.list').addEventListener('click', function (ev) { var gh = ev.target.closest && ev.target.closest('.gh'); if (!gh) return; var id = gh.getAttribute('data-gid'); OPT.expanded[id] = !OPT.expanded[id]; saveOpt(); gh.parentElement.classList.toggle('open', !!OPT.expanded[id]); var arw = gh.querySelector('.arw'); if (arw) arw.textContent = OPT.expanded[id] ? '▼' : '▶'; }); panelB = { host: host, root: root }; } function renderStatus() { if (!panelB) return; var root = panelB.root; var q = function (k) { return root.querySelector('[data-k="' + k + '"]'); }; var ub = root.querySelector('[data-a="undone"]'); if (ub) { ub.textContent = OPT.onlyUndone ? '只看未完成 ✓' : '只看未完成'; ub.className = OPT.onlyUndone ? 'on' : ''; } // 班级下拉 var sel = q('cls'); if (sel && QUEUE.classes.length) { var opts = ''; QUEUE.classes.forEach(function (c) { opts += ''; }); sel.innerHTML = opts; if (QUEUE.tcId) sel.value = QUEUE.tcId; } if (!QUEUE.groups.length) { var tot0 = q('tot'); var list0 = q('list'); if (!token()) { if (tot0) tot0.innerHTML = '未登录'; if (list0) list0.innerHTML = '
请先登录广西干部网络学院
'; } else if (QUEUE.lastError) { if (tot0) tot0.innerHTML = '读取失败'; if (list0) list0.innerHTML = '
' + esc(QUEUE.lastError) + '
'; } else { if (tot0) tot0.innerHTML = '暂无必修课数据'; if (list0) list0.innerHTML = '
当前班级没有必修课,或尚未进入班级
'; } return; } var totalAll = 0, doneAll = 0, hourAll = 0, hourDone = 0; QUEUE.groups.forEach(function (g) { g.lessons.forEach(function (r) { var h = parseFloat(r.classHour || 0); hourAll += h; totalAll++; if (isLessonDone(r)) { doneAll++; hourDone += h; } }); }); var tot = q('tot'); if (tot) { tot.innerHTML = '已完成 ' + doneAll + ' / ' + totalAll + ' 节' + ' 剩余 ' + (totalAll - doneAll) + ' 节
学时 ' + hourDone.toFixed(1) + ' / ' + hourAll.toFixed(1); } var html = []; QUEUE.groups.forEach(function (g) { var done = g.lessons.filter(isLessonDone).length; var total = g.lessons.length; var left = total - done; var pct = total ? Math.round(done / total * 100) : 0; var shown = OPT.onlyUndone ? g.lessons.filter(function (r) { return !isLessonDone(r); }) : g.lessons; var open = !!OPT.expanded[g.id]; var col = left === 0 ? '#63eda6' : (done > 0 ? '#ffd470' : '#ff96a3'); html.push('
'); html.push('
'); html.push('' + (open ? '▼' : '▶') + ''); html.push('' + esc(g.name) + ''); html.push('' + done + '/' + total + (left > 0 ? ' 剩' + left : ' ✓') + ''); html.push('
'); html.push('
'); html.push('
'); if (!shown.length) { html.push('
' + (OPT.onlyUndone ? '该大课已全部完成' : '暂无课程') + '
'); } else { html.push('
'); shown.forEach(function (r, i) { var p = parseFloat(r.learnProgress || 0); var ok = isLessonDone(r); var c = ok ? 'ok' : (p > 0 ? 'mid' : 'no'); html.push('
'); html.push('
' + (i + 1) + '
'); html.push(''); html.push('
' + (ok ? '100%' : p.toFixed(1) + '%') + '
'); html.push('
' + (ok ? '\u2713' : '\u25cb') + '
'); html.push('
'); }); html.push('
'); } html.push('
'); }); var list = q('list'); if (list) list.innerHTML = html.join(''); } var statusLoading = false; function refreshStatus() { if (!panelB || statusLoading) return; statusLoading = true; var tot = panelB.root.querySelector('[data-k="tot"]'); var list = panelB.root.querySelector('[data-k="list"]'); if (tot) tot.textContent = '正在读取…'; if (list) list.innerHTML = '
读取中…
'; var p = QUEUE.built ? Promise.resolve(QUEUE.groups) : (QUEUE.classes.length ? buildQueue(QUEUE.tcId) : fetchTrainClass().then(function (id) { return buildQueue(id); })); p.then(function () { renderStatus(); }).catch(function (e) { QUEUE.lastError = e && e.message ? e.message : String(e); renderStatus(); }).finally(function () { statusLoading = false; }); } // 刷完一门后同步刷新状态面板 function refreshStatusPanels() { if (panelB && QUEUE.built) { QUEUE.built = false; refreshStatus(); } } /* ============================================================ 主循环 ============================================================ */ setInterval(function () { buildLearnPanel(); buildStatusPanel(); if (panelA) { bindVideo(); if (CFG.enabled) { if (!curLessonId || curLessonId !== lessonIdFromUrl()) { lessonInfo = null; curGkey = null; skipChecked = false; skipping = false; loadLessonInfo(function (info) { if (info) startSaveLoop(); }); } dismissPopup(); if (video) { onMeta(); if (Math.abs(video.currentTime - lastT) > 0.1) { lastT = video.currentTime; lastChange = Date.now(); } else if (CFG.stallReloadMs > 0 && !video.ended && Date.now() - lastChange > CFG.stallReloadMs) { if (Date.now() - lastReload > 120000) { lastReload = Date.now(); log('进度停滞,刷新页面'); setTimeout(function () { location.reload(); }, 800); } } } } updateLearnPanel(); } }, CFG.tickMs); /* 启动 */ log('脚本已加载 v8.0.1'); // 状态面板:延迟启动避免与播放页初始化抢资源 setTimeout(function () { buildStatusPanel(); if (!QUEUE.built && !QUEUE.building) { fetchTrainClass().then(function (id) { return buildQueue(id); }) .then(function () { renderStatus(); }) .catch(function (e) { QUEUE.lastError = e && e.message ? e.message : String(e); log('状态初始化失败: ' + QUEUE.lastError); renderStatus(); }); } }, 1500); // 播放页自动建队列 if (lessonIdFromUrl()) { setTimeout(function () { if (!QUEUE.built && !QUEUE.building) buildQueue(); }, 2500); } /* 对外 API */ try { window.__gxela = { cfg: CFG, queue: function () { return QUEUE; }, next: function () { nextLock = false; goNextLesson(true); }, rebuild: function () { QUEUE.built = false; return buildQueue(); }, status: function () { return { logs: logs.slice(-30), groups: QUEUE.groups.map(function (g) { return { name: g.name, total: g.lessons.length, done: g.lessons.filter(isLessonDone).length }; }) }; } }; } catch (e) {} })();