// ==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 = [ '', '