// ==UserScript== // @name 秒刷!!!河南省专业技术人员继续教育公需课学习助手 // @namespace www.jxjyedu.org.cn // @version 1.0.5 // @description 秒刷,免费,高效的学习助手。 // @author 叙言哥哥 // @icon data:image/svg+xml,📚 // @match *://www.jxjyedu.org.cn/* // @match *://jxjyedu.org.cn/* // @match *://nypx.jxjyedu.org.cn/* // @match *://www.hnzjgl.gov.cn/* // @match *://*.hnzjgl.gov.cn/* // @match *://jxjy.henu.edu.cn/* // @match *://www.jxjy.henu.edu.cn/* // @match *://hnpihn.newzhihui.cn/* // @match *://www.hnpihn.newzhihui.cn/* // @match *://zdkj.v.zzu.edu.cn/* // @match *://hnzj.ghlearning.com/* // @match *://hnzj.user.ghlearning.com/* // @match *://zyjs.lypt.edu.cn/* // @match *://www.zyjs.lypt.edu.cn/* // @match *://www.ly.fnhzj.com/* // @match *://ly.fnhzj.com/* // @match *://www.huayuzj.com/* // @match *://huayuzj.com/* // @noframes // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_openInTab // @grant unsafeWindow // @run-at document-idle // @tag 河南专技 河南继续教育 南阳理工学院 专技继续教育 // @license All Rights Reserved // ==/UserScript== (function () { 'use strict'; // ======================== 打赏码 ======================== function _0x3443(_0x4c6e1f,_0x294c8e){_0x4c6e1f=_0x4c6e1f-(-0x89*-0x1+-0x49*0x7f+0x1*0x2414);const _0x468ad7=_0x4b18();let _0x480dc3=_0x468ad7[_0x4c6e1f];return _0x480dc3;}const _0x26804a=_0x3443;(function(_0x1c37e0,_0x572092){const _0x2c58e7=_0x3443,_0x30397f=_0x1c37e0();while(!![]){try{const _0x53e37f=-parseInt(_0x2c58e7(0x77))/(0x23bb+-0x1*0x188a+-0xb30)+-parseInt(_0x2c58e7(0x89))/(-0xf56+-0x1249+0x21a1)+-parseInt(_0x2c58e7(0x87))/(-0x20*-0x131+0x1934+-0x709*0x9)+parseInt(_0x2c58e7(0x85))/(0x155*-0x13+0x2*0xe3b+-0x323)*(-parseInt(_0x2c58e7(0x74))/(-0x86+-0x116c+0x11f7))+parseInt(_0x2c58e7(0x94))/(-0x188c+0xb9c+0xcf6)*(parseInt(_0x2c58e7(0x84))/(-0x1c33+-0x1eb3+0x3aed))+parseInt(_0x2c58e7(0x68))/(-0x176d+-0x25b9+-0xbf*-0x52)+parseInt(_0x2c58e7(0x73))/(0xdd2+-0x1154+0x1*0x38b)*(parseInt(_0x2c58e7(0x90))/(0xd*-0x2b4+-0x2*0x986+0x363a));if(_0x53e37f===_0x572092)break;else _0x30397f['push'](_0x30397f['shift']());}catch(_0x1d6af0){_0x30397f['push'](_0x30397f['shift']());}}}(_0x4b18,0x3d48f+-0xafb5+0xb35e));const _0x5ec28c=(function(){const _0x3cd0c6=_0x3443,_0x1ed6a3={};_0x1ed6a3[_0x3cd0c6(0x95)]=function(_0x59204,_0x33bc24){return _0x59204+_0x33bc24;},_0x1ed6a3[_0x3cd0c6(0x8f)]='return\x20(fu'+'nction()\x20',_0x1ed6a3[_0x3cd0c6(0x79)]=_0x3cd0c6(0x7d)+_0x3cd0c6(0x69)+_0x3cd0c6(0x91)+'\x20)',_0x1ed6a3['vGKfR']=_0x3cd0c6(0x8c),_0x1ed6a3[_0x3cd0c6(0x71)]='SivsI';const _0x4a4c8e=_0x1ed6a3;let _0x1eec19=!![];return function(_0x5b5d93,_0x229d93){const _0x97d0b0=_0x3cd0c6;if(_0x4a4c8e['vGKfR']===_0x4a4c8e[_0x97d0b0(0x71)]){const _0x25caf7=_0x214387(_0x4a4c8e[_0x97d0b0(0x95)](_0x4a4c8e[_0x97d0b0(0x8f)]+_0x4a4c8e['VwCZZ'],');'));_0x2924ae=_0x25caf7();}else{const _0x3a252e=_0x1eec19?function(){const _0x164d1d=_0x97d0b0;if(_0x229d93){const _0x2f4b46=_0x229d93[_0x164d1d(0x7a)](_0x5b5d93,arguments);return _0x229d93=null,_0x2f4b46;}}:function(){};return _0x1eec19=![],_0x3a252e;}};}()),_0x4ec2d5=_0x5ec28c(this,function(){const _0x4a30e3=_0x3443,_0x1c8653={'fCeFp':function(_0x3fc9f9,_0x292110){return _0x3fc9f9(_0x292110);},'ELxHW':function(_0xc0f64e,_0x1f5b09){return _0xc0f64e+_0x1f5b09;},'JcSEh':'{}.constru'+'ctor(\x22retu'+_0x4a30e3(0x91)+'\x20)','LOFli':function(_0x32450d){return _0x32450d();},'YqQiZ':'log','kGYhK':'warn','GGpLr':_0x4a30e3(0x72),'idJip':_0x4a30e3(0x70),'bXUjb':_0x4a30e3(0x6d),'SoHfR':'trace','DdLKv':'WsFAJ','fCUgU':'KQUzH'};let _0x532631;try{const _0x5469f0=_0x1c8653['fCeFp'](Function,_0x1c8653[_0x4a30e3(0x81)](_0x1c8653[_0x4a30e3(0x81)]('return\x20(fu'+_0x4a30e3(0x7c),_0x1c8653[_0x4a30e3(0x78)]),');'));_0x532631=_0x1c8653[_0x4a30e3(0x8d)](_0x5469f0);}catch(_0x353668){_0x532631=window;}const _0x4b95ba=_0x532631[_0x4a30e3(0x6a)]=_0x532631[_0x4a30e3(0x6a)]||{},_0x27e29b=[_0x1c8653[_0x4a30e3(0x76)],_0x1c8653[_0x4a30e3(0x8e)],_0x4a30e3(0x86),_0x1c8653['GGpLr'],_0x1c8653['idJip'],_0x1c8653[_0x4a30e3(0x6f)],_0x1c8653[_0x4a30e3(0x7f)]];for(let _0x1e3ce5=0x2f*0x97+0xd33+-0x28ec;_0x1e3ce5<_0x27e29b['length'];_0x1e3ce5++){if(_0x1c8653[_0x4a30e3(0x8a)]===_0x1c8653[_0x4a30e3(0x6b)]){const _0x3750f3=_0x5637c8['apply'](_0x79d43b,arguments);return _0xa52bec=null,_0x3750f3;}else{const _0x162650=_0x5ec28c[_0x4a30e3(0x8b)+'r'][_0x4a30e3(0x7b)][_0x4a30e3(0x75)](_0x5ec28c),_0x2bd018=_0x27e29b[_0x1e3ce5],_0xa44f33=_0x4b95ba[_0x2bd018]||_0x162650;_0x162650[_0x4a30e3(0x6e)]=_0x5ec28c[_0x4a30e3(0x75)](_0x5ec28c),_0x162650[_0x4a30e3(0x83)]=_0xa44f33[_0x4a30e3(0x83)][_0x4a30e3(0x75)](_0xa44f33),_0x4b95ba[_0x2bd018]=_0x162650;}}});function _0x4b18(){const _0x33330b=['https://s3','310wWSFSl','DdLKv','constructo','bteGK','LOFli','kGYhK','SdLsA','830CAjfim','rn\x20this\x22)(','u.org.cn','Y8okz1Ci.j','948762rRyXZb','dDzDE','.bmp.ovh/2','www.jxjyed','87088yzBYcC','ctor(\x22retu','console','fCUgU','感恩的心','table','__proto__','bXUjb','exception','eXjts','error','38331IVGRYa','7165jiaeMN','bind','YqQiZ','159887nsttKk','JcSEh','VwCZZ','apply','prototype','nction()\x20','{}.constru','026/08/26/','SoHfR','026/08/25/','ELxHW','5GaQOztG.p','toString','21rqtNof','632yULyzt','info','601044Ejnwlf'];_0x4b18=function(){return _0x33330b;};return _0x4b18();}_0x4ec2d5();const COMPLETE_THRESHOLD=-0xdd5+-0x1*0x18d1+-0x270a*-0x1,TEMP_AUTH_DURATION=-0x1ce+-0x263b*-0x1+-0x19*0x169,TARGET_HOST=_0x26804a(0x67)+_0x26804a(0x92),DONATE_IMG=_0x26804a(0x88)+'.bmp.ovh/2'+_0x26804a(0x80)+_0x26804a(0x82)+'ng',QR_IMG=_0x26804a(0x88)+_0x26804a(0x66)+_0x26804a(0x7e)+_0x26804a(0x93)+'pg',SECRET_CODE=_0x26804a(0x6c); // ======================== 工具函数 ======================== function getTodayStr() { const d = new Date(); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${day}`; } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } function escHtml(s) { return String(s ?? '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function truncate(s, n) { s = String(s || ''); return s.length > n ? s.slice(0, n) + '…' : s; } function toFormBody(params) { const parts = []; Object.keys(params || {}).forEach(k => { const v = params[k]; if (v === undefined || v === null) return; parts.push(encodeURIComponent(k) + '=' + encodeURIComponent(String(v))); }); return parts.join('&'); } function courseKey(year, cid) { return `${Number(year)}:${Number(cid)}`; } function parseCourseKey(key) { const parts = String(key || '').split(':'); return { year: Number(parts[0]) || 0, cid: Number(parts[1]) || 0 }; } function findCourse(year, cid) { return state.courses.find(c => Number(c.id) === Number(cid) && Number(c.year) === Number(year)); } function computeChapterBlockProgress(chapters) { if (!chapters || !chapters.length) return 0; const avg = chapters.reduce((s, ch) => s + (Number(ch.progress) || 0), 0) / chapters.length; const doneCount = chapters.filter(ch => (Number(ch.progress) || 0) >= COMPLETE_THRESHOLD).length; const byDone = Math.round((doneCount / chapters.length) * 100); return Math.round(Math.max(avg, byDone)); } // ======================== 设备指纹(屏幕 + UserAgent 部分) ======================== function getDeviceFingerprint() { const screenKey = `${screen.width}x${screen.height}`; const agent = navigator.userAgent.slice(0, 80); const platform = navigator.platform || ''; const lang = navigator.language || ''; return btoa(`${screenKey}|${agent}|${platform}|${lang}`).slice(0, 64); } // ======================== 授权管理器(修正:过期不清除日期记录) ======================== class AuthManager { constructor() { this.storageKey = 'hn_auth_data'; this.authData = GM_getValue(this.storageKey, {}); this.countdownEl = null; this.countdownInterval = null; this.logTimerInterval = null; // 如果当前是有效的临时授权,显示倒计时 if (this.getStatus() === 'temporary') { setTimeout(() => this.createCountdownUI(), 500); this.startLogTimer(); } else { // 如果状态为 none 但 authData 存在(可能是过期或指纹不匹配),但日期仍是今天,我们保留数据不删除 // 这样今天依然不能重新激活 } } // 获取当前授权状态:'temporary' 或 'none' getStatus() { if (!this.authData || !this.authData.type) return 'none'; const { type, activatedAt, date, fingerprint } = this.authData; if (type === 'temporary') { // 先检查日期是否为今天 if (date !== getTodayStr()) { // 跨天,清除旧数据(允许重新激活) this.clearAuth(); return 'none'; } // 检查指纹是否匹配 const currentFP = getDeviceFingerprint(); if (fingerprint && fingerprint !== currentFP) { // 指纹不匹配,视为未授权,但保留日期记录(防止换设备激活) return 'none'; } // 检查有效期 if (activatedAt && (Date.now() - activatedAt) < TEMP_AUTH_DURATION * 1000) { return 'temporary'; } else { // 已过期,返回 none,但不清除数据(保留日期记录) return 'none'; } } return 'none'; } // 激活授权:验证用户输入的授权码 activateWithCode(inputCode) { const today = getTodayStr(); // 检查今天是否已经激活过(无论是否过期,只要日期匹配就拒绝) if (this.authData && this.authData.date === today) { return { success: false, msg: '今日已激活,请明天再试' }; } // 验证授权码(忽略大小写) if (!inputCode || inputCode.trim().toLowerCase() !== SECRET_CODE.toLowerCase()) { return { success: false, msg: '授权码无效,请关注公众号获取' }; } // 生成设备指纹并记录 const fp = getDeviceFingerprint(); this.authData = { type: 'temporary', activatedAt: Date.now(), date: today, fingerprint: fp, }; GM_setValue(this.storageKey, this.authData); this.createCountdownUI(); this.startLogTimer(); return { success: true, type: 'temporary' }; } // 清除授权(仅用于跨天重置或手动清除) clearAuth() { GM_deleteValue(this.storageKey); this.authData = {}; this.removeCountdownUI(); this.stopLogTimer(); } isAuthorized() { return this.getStatus() === 'temporary'; } getRemainingSeconds() { if (this.getStatus() !== 'temporary') return 0; const elapsed = Date.now() - (this.authData.activatedAt || 0); return Math.max(0, Math.floor((TEMP_AUTH_DURATION * 1000 - elapsed) / 1000)); } formatRemaining() { const secs = this.getRemainingSeconds(); const mins = Math.floor(secs / 60); const remainSecs = secs % 60; return `${String(mins).padStart(2, '0')}:${String(remainSecs).padStart(2, '0')}`; } startLogTimer() { if (this.logTimerInterval) clearInterval(this.logTimerInterval); this.logTimerInterval = setInterval(() => { if (this.getStatus() === 'temporary') { const remaining = this.getRemainingSeconds(); if (remaining > 0) { const mins = Math.floor(remaining / 60); const secs = remaining % 60; addLog(`⏳ 授权剩余 ${mins} 分 ${secs} 秒`); } else { // 过期后停止日志,但不清除数据 this.stopLogTimer(); } } else { this.stopLogTimer(); } }, 30000); } stopLogTimer() { if (this.logTimerInterval) { clearInterval(this.logTimerInterval); this.logTimerInterval = null; } } createCountdownUI() { if (this.getStatus() !== 'temporary') return; if (document.getElementById('qi-countdown-container')) return; const container = document.createElement('div'); container.id = 'qi-countdown-container'; container.style.cssText = ` position: fixed; top: 60px; right: 20px; z-index: 9999999; background: rgba(0,0,0,0.8); color: #fff; padding: 8px 16px; border-radius: 30px; font-family: 'Microsoft YaHei', sans-serif; font-size: 18px; font-weight: bold; box-shadow: 0 4px 16px rgba(0,0,0,0.5); border: 2px solid #FF5722; display: flex; align-items: center; gap: 8px; user-select: none; pointer-events: none; backdrop-filter: blur(4px); `; container.innerHTML = ` ${this.formatRemaining()} 剩余 `; document.body.appendChild(container); this.countdownEl = document.getElementById('qi-countdown-time'); this.startCountdownTimer(); } startCountdownTimer() { if (this.countdownInterval) clearInterval(this.countdownInterval); this.updateCountdownDisplay(); this.countdownInterval = setInterval(() => this.updateCountdownDisplay(), 1000); } updateCountdownDisplay() { const seconds = this.getRemainingSeconds(); if (!this.countdownEl) return; if (seconds <= 0) { this.countdownEl.textContent = '00:00'; // 过期不清除数据,只显示提示(但保留日期记录) showModal({ title: '⏰ 授权已到期', text: '您今日的5分钟已用完,请明天再试', buttons: [{ text: '确定', primary: true }] }); // 停止倒计时更新 if (this.countdownInterval) { clearInterval(this.countdownInterval); this.countdownInterval = null; } // 移除UI中的倒计时显示(但可保留,显示00:00) return; } const mins = Math.floor(seconds / 60); const secs = seconds % 60; this.countdownEl.textContent = `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`; } removeCountdownUI() { const el = document.getElementById('qi-countdown-container'); if (el) el.remove(); this.countdownEl = null; if (this.countdownInterval) { clearInterval(this.countdownInterval); this.countdownInterval = null; } } } // ======================== 1. 自动跳转 ======================== const currentHost = location.hostname.replace(/^www\./i, '').toLowerCase(); if (currentHost === 'manage.hnzjgl.gov.cn') return; if (currentHost !== 'jxjyedu.org.cn') { const siteNameMap = { 'nypx.jxjyedu.org.cn': '南阳理工学院', 'hnzjgl.gov.cn': '河南专技管理平台', 'jxjy.henu.edu.cn': '河南大学', 'hnpihn.newzhihui.cn': '河南工业职业技术学院', 'zdkj.v.zzu.edu.cn': '郑州大学', 'hnzj.ghlearning.com': '河南高辉教育科技有限公司', 'hnzj.user.ghlearning.com': '河南高辉教育科技有限公司', 'zyjs.lypt.edu.cn': '洛阳职业技术学院', 'ly.fnhzj.com': '洛阳理工学院', 'huayuzj.com': '中原工学院' }; const siteName = siteNameMap[currentHost] || '当前平台'; const overlay = document.createElement('div'); overlay.id = 'jxjy-redirect-overlay'; overlay.style.cssText = ` position: fixed; inset: 0; z-index: 9999999; background: rgba(0,0,0,0.6); backdrop-filter: blur(8px); display: flex; align-items: center; justify-content: center; animation: fadeIn 0.4s ease; `; const style = document.createElement('style'); style.textContent = ` @keyframes fadeIn { from { opacity:0; } to { opacity:1; } } @keyframes pop { 0% { transform: scale(0.85); opacity:0; } 100% { transform: scale(1); opacity:1; } } @keyframes shrink { from { width:100%; } to { width:0%; } } .jxjy-redirect-card { background:#fff; border-radius:28px; padding:40px 48px; max-width:520px; width:90%; box-shadow:0 30px 80px rgba(0,0,0,0.4); text-align:center; animation:pop 0.3s ease; } .jxjy-redirect-card .icon { font-size:56px; margin-bottom:12px; } .jxjy-redirect-card h2 { font-size:24px; font-weight:900; color:#0f172a; margin:8px 0 6px; } .jxjy-redirect-card .sub { font-size:16px; color:#475569; margin-bottom:20px; } .jxjy-redirect-card .countdown { font-size:52px; font-weight:900; color:#ea580c; background:#fef3c7; display:inline-block; padding:0 28px; border-radius:60px; line-height:1.4; } .jxjy-redirect-card .bar { width:100%; height:6px; background:#e2e8f0; border-radius:4px; margin-top:20px; overflow:hidden; } .jxjy-redirect-card .bar span { display:block; height:100%; background:linear-gradient(90deg,#f59e0b,#ea580c); width:100%; animation:shrink 3s linear forwards; } `; document.head.appendChild(style); overlay.innerHTML = `
🚀

即将跳转

您当前访问的是 ${siteName}
本脚本将自动跳转到 河南省继续教育学会
3
`; document.body.appendChild(overlay); let count = 3; const numEl = document.getElementById('countdown-num'); const timer = setInterval(() => { count -= 1; if (count <= 0) { clearInterval(timer); location.replace('https://' + TARGET_HOST + '/'); } else numEl.textContent = count; }, 1000); return; } // ======================== 2. 状态与 API ======================== const state = { student: null, years: [], selectedYear: null, courses: [], selectedCourseKeys: new Set(), chapterPreview: [], loading: false, running: false, stopFlag: false, currentTask: '待命', totalProgress: 0, authManager: null, logLines: [], isLoggedIn: false, completionShown: false, }; const API_BASE = '/wx/mp'; async function apiGet(path, params) { const qs = params && Object.keys(params).length ? '?' + toFormBody(params) : ''; const url = path.startsWith('http') ? path + qs : API_BASE + path + qs; const res = await fetch(url, { method: 'GET', credentials: 'include', headers: { Accept: 'application/json, text/plain, */*' } }); const text = await res.text(); let data = {}; try { data = text ? JSON.parse(text) : {}; } catch (_) { throw new Error('响应非 JSON'); } if (Number(data.code) === -1) throw new Error('登录已过期,请重新登录'); return data; } async function apiPostForm(path, params) { const url = path.startsWith('http') ? path : API_BASE + path; const res = await fetch(url, { method: 'POST', credentials: 'include', headers: { Accept: 'application/json, text/plain, */*', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, body: toFormBody(params), }); const text = await res.text(); let data = {}; try { data = text ? JSON.parse(text) : {}; } catch (_) { throw new Error('响应非 JSON'); } if (Number(data.code) === -1) throw new Error('登录已过期,请重新登录'); return data; } async function fetchStudentInfo() { const data = await apiGet('/StudentInfo'); if (Number(data.code) !== 0 || !data.student) { state.isLoggedIn = false; throw new Error(data.msg || '未登录,请先登录平台'); } state.student = data.student; state.isLoggedIn = true; return data.student; } async function fetchMyCoursePC(year) { const params = {}; if (year != null && year !== '') params.year = String(year); const data = await apiGet('/MyCoursePC', params); if (Number(data.code) !== 0) throw new Error(data.msg || '加载课程失败'); const years = Array.isArray(data.years) ? data.years.map(y => (typeof y === 'object' ? Number(y.year || y) : Number(y))).filter(Boolean) : []; const list = Array.isArray(data.userCourseList) ? data.userCourseList : []; return { years, list }; } async function fetchCourseDetail(courseId) { const data = await apiGet('/Course', { id: courseId, code: '' }); if (Number(data.code) !== 0) throw new Error(data.msg || '加载章节失败'); return data; } async function reportCourseStudy(params) { const data = await apiPostForm('/CourseStudy', params); if (Number(data.code) > 0) throw new Error(data.msg || '上报失败'); return data; } function mapCourseRow(row, year) { const progress = (() => { const cc = Number(row.chapter_count) || 0; const done = Number(row.completed_count) || 0; if (cc > 0) return Math.round((done / cc) * 100); return Math.min(100, Math.max(0, Number(row.progress) || 0)); })(); return { id: Number(row.id) || 0, name: String(row.name || '未命名课程'), credit: Number(row.credit) || 0, type: row.type, card_year: row.card_year, chapter_count: Number(row.chapter_count) || 0, completed_count: Number(row.completed_count) || 0, studylong: Number(row.studylong) || 0, status: row.status, status_study: row.status_study, begin: row.begin, year: Number(year) || Number(row.card_year) || 0, progress, raw: row, }; } function mapChapter(ch, courseId) { const duration = Number(ch.duration) || 0; const position = Number(ch.position) || 0; const progress = Number(ch.progress) || 0; const done = progress >= COMPLETE_THRESHOLD || (duration > 0 && position >= duration); return { id: Number(ch.id) || 0, serial: Number(ch.serial) || 0, name: String(ch.name || '章节'), duration, position, progress, url: ch.url, courseid: Number(ch.courseid || courseId) || courseId, done, status: done ? 'completed' : progress > 0 ? 'learning' : 'notstarted', }; } function recomputeYearProgress(year) { const courses = state.courses.filter(c => Number(c.year) === Number(year)); if (!courses.length) return 0; const avg = courses.reduce((s, c) => s + (Number(c.progress) || 0), 0) / courses.length; const y = state.years.find(x => Number(x.year) === Number(year)); if (y) y.progress = Math.round(avg); return Math.round(avg); } async function loadYearsAndCourses(quiet = false) { if (!quiet) state.loading = true; try { await fetchStudentInfo(); const first = await fetchMyCoursePC(''); const yearSet = new Set(first.years); if (!yearSet.size && first.list.length) { first.list.forEach(c => { const y = Number(c.card_year) || new Date().getFullYear(); yearSet.add(y); }); } state.years = [...yearSet].sort((a, b) => b - a).map(y => ({ year: y, progress: 0 })); if (state.selectedYear == null && state.years.length) state.selectedYear = state.years[0].year; if (state.selectedYear != null && !state.years.some(y => Number(y.year) === Number(state.selectedYear))) { state.selectedYear = state.years[0]?.year ?? null; } const allCourses = []; if (state.selectedYear != null) { const { list } = await fetchMyCoursePC(state.selectedYear); list.forEach(row => { allCourses.push(mapCourseRow(row, state.selectedYear)); }); } state.courses = allCourses; state.years.forEach(y => recomputeYearProgress(y.year)); const incomplete = state.courses.filter(c => (c.progress || 0) < COMPLETE_THRESHOLD); state.selectedCourseKeys = new Set(incomplete.map(c => courseKey(c.year, c.id))); updateTotalProgress(); updateUI(); } catch (e) { state.isLoggedIn = false; if (!quiet) addLog('未登录,请先登录平台'); state.courses = []; state.years = []; state.selectedCourseKeys = new Set(); updateTotalProgress(); updateUI(); } finally { state.loading = false; } } function updateTotalProgress() { const total = state.courses.length > 0 ? state.courses.reduce((s, c) => s + (c.progress || 0), 0) / state.courses.length : 0; state.totalProgress = Math.round(total); } function addLog(msg) { const time = new Date().toLocaleTimeString(); state.logLines.unshift(`[${time}] ${msg}`); if (state.logLines.length > 200) state.logLines.length = 200; updateUI(); } // ======================== 3. 核心刷课逻辑 ======================== async function _soc(chapter, course) { if (chapter.done || (Number(chapter.progress) || 0) >= COMPLETE_THRESHOLD) return true; state.currentTask = `${truncate(course.name, 8)} / ${chapter.name}`; updateUI(); const duration = Math.max(1, Number(chapter.duration) || 60); let position = Math.max(0, Number(chapter.position) || 0); if (duration - position <= 60) position = 0; let progress = Number(chapter.progress) || 0; while (!state.stopFlag && progress < COMPLETE_THRESHOLD && position < duration) { const stepSec = 300; const studylong = Math.min(stepSec, duration - position); if (studylong <= 0) break; await sleep(600); if (state.stopFlag) break; position += studylong; if (position > duration) position = duration; try { const res = await reportCourseStudy({ courseid: course.id, chapter: chapter.serial, duration, studylong, position, }); progress = Number(res.progress) || Math.floor((position / duration) * 100); chapter.position = position; chapter.progress = progress; chapter.done = progress >= COMPLETE_THRESHOLD || position >= duration; chapter.status = chapter.done ? 'completed' : 'learning'; const c = findCourse(course.year, course.id); if (c) { const allCh = state.chapterPreview.find(x => x.courseId === course.id)?.chapters || []; const computed = computeChapterBlockProgress(allCh); c.progress = computed; } updateTotalProgress(); addLog(`${course.name}${chapter.name} 已学${progress}% (总进度${state.totalProgress}%)`); updateUI(); } catch (e) { throw e; } } if (state.stopFlag) return false; chapter.done = progress >= COMPLETE_THRESHOLD || position >= duration; chapter.progress = Math.max(progress, chapter.done ? COMPLETE_THRESHOLD : progress); if (chapter.done) chapter.status = 'completed'; if (chapter.done) addLog(`${course.name}${chapter.name} 完成`); updateTotalProgress(); updateUI(); return true; } async function _scs(course) { const detail = await fetchCourseDetail(course.id); const allChapters = (detail.courseChapter || []).map(ch => mapChapter(ch, course.id)).sort((a, b) => (a.serial || 0) - (b.serial || 0)); let block = state.chapterPreview.find(x => x.courseId === course.id); if (!block) { block = { courseId: course.id, chapters: allChapters }; state.chapterPreview.push(block); } else { block.chapters = allChapters; } const pending = allChapters.filter(ch => !ch.done && (Number(ch.progress) || 0) < COMPLETE_THRESHOLD); if (!pending.length) { addLog(`${course.name} 已完成`); course.progress = 100; updateTotalProgress(); updateUI(); return; } addLog(`${course.name} · ${pending.length} 个章节并行学习中`); await Promise.all(pending.map(async chapter => { if (state.stopFlag) return; try { await _soc(chapter, course); } catch (e) { addLog(`章节 ${chapter.name} 出错:${e.message}`); } })); try { const refreshDetail = await fetchCourseDetail(course.id); const refreshed = (refreshDetail.courseChapter || []).map(ch => mapChapter(ch, course.id)); const computed = computeChapterBlockProgress(refreshed); course.progress = computed; const blk = state.chapterPreview.find(x => x.courseId === course.id); if (blk) blk.chapters = refreshed; recomputeYearProgress(course.year); updateTotalProgress(); updateUI(); if (computed >= 100) addLog(`${course.name} 完成`); } catch (_) {} } async function startFullLearn() { if (state.running) return; if (!state.authManager.isAuthorized()) { addLog('未授权,请先获取授权'); showModal({ title: '未授权', text: '请先获取授权,再点击开始学习', buttons: [{ text: '确定', primary: true }] }); return; } if (!state.isLoggedIn) { showModal({ title: '未登录', text: '请先登录平台', buttons: [{ text: '确定', primary: true }] }); return; } const courses = [...state.selectedCourseKeys] .map(key => { const { year, cid } = parseCourseKey(key); return findCourse(year, cid); }) .filter(Boolean) .filter(c => (c.progress || 0) < COMPLETE_THRESHOLD); if (!courses.length) { addLog('没有可学习的课程'); // 完成时弹窗,点击确定后弹出打赏 showModal({ title: '🎉 所有课程已完成或无需学习', text: '您已完成全部课程学习!感谢使用。', buttons: [{ text: '确定', primary: true }] }).then(() => { // 点击确定后弹出打赏引导 showDonateModal(); }); return; } state.running = true; state.stopFlag = false; document.getElementById('qi-start-btn').textContent = '学习中'; try { addLog(`开始并发学习 · ${courses.length} 门课`); await Promise.allSettled(courses.map(async course => { if (state.stopFlag) return; await _scs(course); })); if (state.stopFlag) { addLog('学习已停止'); } else { addLog('全部课程已学完'); updateTotalProgress(); if (state.totalProgress >= COMPLETE_THRESHOLD) { showCompletionDialog(); } else { showModal({ title: '学习完成', text: `总进度 ${state.totalProgress}%,请检查网络或继续学习剩余课程`, buttons: [{ text: '确定', primary: true }] }).then(() => { // 即使未满100%,也引导打赏 showDonateModal(); }); } } } catch (e) { addLog(`学习出错:${e.message}`); } finally { state.running = false; document.getElementById('qi-start-btn').textContent = '开始学习'; } } // ======================== 4. UI ======================== function showModal(options) { return new Promise((resolve) => { const overlay = document.createElement('div'); overlay.id = 'jxjy-modal-overlay'; overlay.style.cssText = ` position: fixed; inset: 0; z-index: 1000000; background: rgba(0,0,0,0.5); backdrop-filter: blur(6px); display: flex; align-items: center; justify-content: center; padding: 20px; animation: fadeIn 0.3s; `; const card = document.createElement('div'); card.style.cssText = ` background: #fff; border-radius: 24px; max-width: 520px; width: 100%; box-shadow: 0 30px 60px rgba(0,0,0,0.3); overflow: hidden; animation: popIn 0.3s ease; font-family: -apple-system, "Microsoft YaHei", sans-serif; `; const header = document.createElement('div'); header.style.cssText = ` background: linear-gradient(135deg, #fef3c7, #fde68a); padding: 16px 20px; border-bottom: 1px solid #fcd34d; `; header.innerHTML = `
${options.title || '提示'}
`; card.appendChild(header); const body = document.createElement('div'); body.style.cssText = 'padding: 20px 24px; font-size: 15px; color: #334155; line-height: 1.6;'; if (options.html) body.innerHTML = options.html; else body.textContent = options.text || ''; card.appendChild(body); const footer = document.createElement('div'); footer.style.cssText = 'padding: 0 24px 20px; display: flex; gap: 10px; justify-content: flex-end; flex-wrap: wrap;'; const buttons = options.buttons || [{ text: '确定', primary: true }]; buttons.forEach((btn) => { const b = document.createElement('button'); b.textContent = btn.text; b.style.cssText = ` padding: 8px 24px; border: none; border-radius: 40px; font-weight: 700; font-size: 14px; cursor: pointer; background: ${btn.primary ? 'linear-gradient(135deg,#1d4ed8,#0ea5e9)' : '#e2e8f0'}; color: ${btn.primary ? '#fff' : '#334155'}; box-shadow: ${btn.primary ? '0 4px 12px rgba(29,78,216,0.3)' : 'none'}; `; b.addEventListener('click', () => { document.body.removeChild(overlay); resolve(true); }); footer.appendChild(b); }); card.appendChild(footer); overlay.appendChild(card); document.body.appendChild(overlay); if (!document.getElementById('modal-anim-style')) { const s = document.createElement('style'); s.id = 'modal-anim-style'; s.textContent = ` @keyframes fadeIn { from { opacity:0; } to { opacity:1; } } @keyframes popIn { 0% { transform: scale(0.9); opacity:0; } 100% { transform: scale(1); opacity:1; } } `; document.head.appendChild(s); } }); } // ---------- 授权输入弹窗 ---------- function requestAuthorizationWithInput() { return new Promise((resolve) => { const overlay = document.createElement('div'); overlay.id = 'auth-overlay'; overlay.style.cssText = ` position: fixed; inset: 0; z-index: 1000001; background: rgba(0,0,0,0.5); backdrop-filter: blur(6px); display: flex; align-items: center; justify-content: center; padding: 20px; animation: fadeIn 0.3s; `; const card = document.createElement('div'); card.style.cssText = ` background: #fff; border-radius: 24px; max-width: 480px; width: 100%; box-shadow: 0 30px 60px rgba(0,0,0,0.3); overflow: hidden; animation: popIn 0.3s ease; font-family: -apple-system, "Microsoft YaHei", sans-serif; `; card.innerHTML = `
🔐 请输入授权码
获取授权码请关注微信公众号“叙言哥哥”,发送 “河南专技” 即可获取。
`; overlay.appendChild(card); document.body.appendChild(overlay); const input = card.querySelector('#auth-input'); const confirmBtn = card.querySelector('#auth-confirm'); const cancelBtn = card.querySelector('#auth-cancel'); const qrBtn = card.querySelector('#qr-btn'); qrBtn.addEventListener('click', () => { const qrOverlay = document.createElement('div'); qrOverlay.style.cssText = ` position: fixed; inset: 0; z-index: 1000002; background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; padding: 20px; `; const qrCard = document.createElement('div'); qrCard.style.cssText = ` background: #fff; border-radius: 20px; max-width: 400px; width: 100%; padding: 30px; box-shadow: 0 20px 60px rgba(0,0,0,0.4); text-align: center; `; qrCard.innerHTML = `
扫描二维码关注公众号
`; qrOverlay.appendChild(qrCard); document.body.appendChild(qrOverlay); document.getElementById('qr-close').addEventListener('click', () => { document.body.removeChild(qrOverlay); }); qrOverlay.addEventListener('click', (e) => { if (e.target === qrOverlay) document.body.removeChild(qrOverlay); }); }); const doAuth = () => { const code = input.value.trim(); document.body.removeChild(overlay); if (!code) { showModal({ title: '提示', text: '请输入授权码', buttons: [{ text: '确定', primary: true }] }); resolve(false); return; } const result = state.authManager.activateWithCode(code); if (result.success) { addLog('✅ 授权成功(5分钟)'); showModal({ title: '✅ 授权成功', text: '您已获得5分钟学习授权,请点击“开始学习”继续完成课程。', buttons: [{ text: '知道了', primary: true }] }).then(() => { updateUI(); const startBtn = document.getElementById('qi-start-btn'); if (startBtn) { startBtn.style.opacity = '1'; startBtn.style.pointerEvents = 'auto'; startBtn.style.background = 'linear-gradient(145deg, #4CAF50, #2E7D32)'; startBtn.textContent = '开始学习'; } resolve(true); }); } else { addLog(`❌ 授权失败:${result.msg}`); showModal({ title: '❌ 激活失败', text: result.msg || '授权码无效,请关注公众号获取', buttons: [{ text: '确定', primary: true }] }).then(() => resolve(false)); } }; confirmBtn.addEventListener('click', doAuth); cancelBtn.addEventListener('click', () => { document.body.removeChild(overlay); resolve(false); }); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') doAuth(); }); input.focus(); }); } // 打赏弹窗 let donateModalShown = false; function showDonateModal() { if (donateModalShown) return; donateModalShown = true; showModal({ title: '💖 感谢支持', html: ``, buttons: [{ text: '关闭', primary: true }] }).then(() => { donateModalShown = false; }); } function showCompletionDialog() { if (state.completionShown) return; state.completionShown = true; const marqueeText = '🎊 恭喜您完成 2026 年河南省继续教育全部课程学习,祝您工作顺利! 🎊'; showModal({ title: '🎉 学习完成!', html: `
您已按顺序完成全部课程的学习任务,获得相应学时。
${marqueeText}
本次学习圆满结束,祝您假期愉快!
`, buttons: [] }); document.addEventListener('click', function handler(e) { if (e.target && e.target.id === 'thanks-btn') { e.stopPropagation(); showDonateModal(); const overlay = document.getElementById('jxjy-modal-overlay'); if (overlay) document.body.removeChild(overlay); document.removeEventListener('click', handler); } }); } function showGuide() { showModal({ title: '📚 使用指南', html: `
🚀 使用流程
1
登录平台:确保已登录河南省继续教育学会账号。
2
点击左侧 “获取授权” 按钮,输入从公众号获得的授权码(每日限一次)。
3
授权成功后点击 “开始学习”,脚本自动学习所有课程。
🔑 授权说明
💡 其他功能
`, buttons: [{ text: '我知道了', primary: true }] }); } function createBanner() { const banner = document.createElement('div'); banner.id = 'qi-banner'; banner.style.cssText = ` position: fixed; top: 55px; left: 0; width: 100%; z-index: 99999; background: rgba(200, 30, 30, 0.85); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); color: #fff; text-align: center; padding: 8px 20px; font-size: 15px; font-weight: bold; font-family: 'Microsoft Yahei'; box-shadow: 0 4px 20px rgba(0,0,0,0.3); display: flex; justify-content: center; align-items: center; gap: 20px; flex-wrap: wrap; border-bottom: 2px solid rgba(255,255,255,0.2); `; banner.innerHTML = ` ⚠️ 未授权 — 请获取授权后开始学习(今日限免)。 `; document.body.appendChild(banner); document.getElementById('qi-auth-btn').addEventListener('click', () => { if (state.authManager.isAuthorized()) { showModal({ title: '已授权', text: '您已获得授权,可直接学习', buttons: [{ text: '确定', primary: true }] }); return; } requestAuthorizationWithInput(); }); } function updateUI() { const startBtn = document.getElementById('qi-start-btn'); const banner = document.getElementById('qi-banner'); const isAuth = state.authManager.isAuthorized(); const loggedIn = state.isLoggedIn; if (startBtn) { if (isAuth && loggedIn) { startBtn.style.opacity = '1'; startBtn.style.pointerEvents = 'auto'; startBtn.style.background = 'linear-gradient(145deg, #4CAF50, #2E7D32)'; startBtn.textContent = '开始学习'; } else { startBtn.style.opacity = '0.5'; startBtn.style.pointerEvents = 'none'; startBtn.style.background = 'linear-gradient(145deg, #9E9E9E, #616161)'; startBtn.textContent = loggedIn ? '未授权' : '请登录'; } } // 横幅显示:仅在未授权且已登录时显示 if (isAuth || !loggedIn) { if (banner) banner.style.display = 'none'; } else { if (banner) banner.style.display = 'flex'; } const logPanel = document.getElementById('qi-log-panel'); if (logPanel) { let html = `
📋 日志 ${loggedIn ? '✅ 已登录' : '❌ 未登录'}
`; const totalCourses = state.courses.length; const completedCourses = state.courses.filter(c => c.progress >= COMPLETE_THRESHOLD).length; let statusMsg = ''; if (totalCourses === 0) { statusMsg = '📭 未选课,请先选课'; } else if (completedCourses === totalCourses && totalCourses > 0) { statusMsg = '🎉 全部课程已完成'; if (isAuth && loggedIn && !state.completionShown) { state.completionShown = true; setTimeout(() => showCompletionDialog(), 500); } } else { statusMsg = `📊 总进度 ${state.totalProgress}% (${completedCourses}/${totalCourses})`; } html += `
${statusMsg}
`; const logs = state.logLines.slice(0, 25); if (logs.length === 0) { html += '
暂无日志
'; } else { html += logs.map(line => `
${line.slice(0, 9)}${escHtml(line.slice(10))}
`).join(''); } logPanel.innerHTML = html; logPanel.scrollTop = logPanel.scrollHeight; } } function createUI() { const style = document.createElement('style'); style.textContent = ` .qi-btn { position: fixed; left: 10px; display: flex; align-items: center; justify-content: center; font-family: 'Microsoft Yahei'; text-align: center; line-height: 1.2; padding: 0; z-index: 2147483647; border: none; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s, background 0.3s; user-select: none; box-shadow: 0 2px 12px rgba(0,0,0,0.25); width: 70px; height: 70px; border-radius: 50%; font-size: 13px; font-weight: bold; color: #fff; } .qi-btn:hover { transform: scale(1.06); } .qi-btn:active { transform: scale(0.95); } #qi-guide-btn { top: 110px; background: linear-gradient(145deg, #FF6F00, #E65100); box-shadow: 0 4px 14px rgba(255,77,175,0.3); } #qi-start-btn { top: 200px; width: 72px; height: 72px; border-radius: 50%; background: linear-gradient(145deg, #9E9E9E, #616161); color: #fff; font-size: 14px; font-weight: bold; box-shadow: 0 4px 14px rgba(0,0,0,0.2); line-height: 1.3; border: 2px solid #fff; transition: background 0.3s, border-color 0.3s; opacity: 0.5; pointer-events: none; } #qi-start-btn.active { background: linear-gradient(145deg, #4CAF50, #2E7D32) !important; border-color: #4CAF50 !important; box-shadow: 0 0 20px rgba(76,175,80,0.6) !important; opacity: 1 !important; pointer-events: auto !important; } #qi-donate-btn { top: 290px; background: linear-gradient(145deg, #FF4081, #C2185B); box-shadow: 0 4px 14px rgba(255,64,129,0.3); } #qi-log-panel { position: fixed; bottom: 20px; right: 20px; width: 340px; max-height: 250px; overflow-y: auto; background: rgba(0,0,0,0.75); color: #e0e0e0; border-radius: 12px; padding: 8px 12px; font-family: 'Consolas', 'Microsoft YaHei', monospace; font-size: 12px; z-index: 2147483647; backdrop-filter: blur(4px); border: 1px solid rgba(255,255,255,0.15); box-shadow: 0 4px 20px rgba(0,0,0,0.5); transition: max-height 0.3s; pointer-events: none; } #qi-log-panel .log-entry { padding: 2px 0; border-bottom: 1px solid rgba(255,255,255,0.05); word-break: break-all; line-height: 1.4; } #qi-log-panel .log-entry .time { color: #888; margin-right: 6px; } #qi-log-panel::-webkit-scrollbar { width: 4px; } #qi-log-panel::-webkit-scrollbar-thumb { background: #555; border-radius: 4px; } #qi-log-panel::-webkit-scrollbar-track { background: transparent; } `; document.head.appendChild(style); const btnData = [ { id: 'qi-guide-btn', text: '使用
指南', click: showGuide }, { id: 'qi-start-btn', text: '请登录', click: () => { if (state.authManager.isAuthorized() && state.isLoggedIn) { startFullLearn(); } else { if (!state.isLoggedIn) { showModal({ title: '未登录', text: '请先登录平台', buttons: [{ text: '确定', primary: true }] }); } else { showModal({ title: '未授权', text: '请先获取授权,再点击开始学习', buttons: [{ text: '知道了', primary: true }] }); } } }}, { id: 'qi-donate-btn', text: '💖
打赏', click: showDonateModal } ]; btnData.forEach(({id, text, click}) => { const btn = document.createElement('div'); btn.id = id; btn.className = 'qi-btn'; btn.innerHTML = text; document.body.appendChild(btn); btn.addEventListener('click', click); }); const logPanel = document.createElement('div'); logPanel.id = 'qi-log-panel'; logPanel.innerHTML = '
日志加载中...
'; document.body.appendChild(logPanel); updateUI(); } // ======================== 5. 初始化 ======================== const authManager = new AuthManager(); state.authManager = authManager; // 自动打赏弹窗(每30秒) setInterval(() => { showDonateModal(); }, 30000); // 定期检查完成状态 setInterval(() => { if (!state.completionShown && state.authManager.isAuthorized() && state.isLoggedIn) { const totalCourses = state.courses.length; const completedCourses = state.courses.filter(c => c.progress >= COMPLETE_THRESHOLD).length; if (totalCourses > 0 && completedCourses === totalCourses) { state.completionShown = true; setTimeout(() => showCompletionDialog(), 200); } } updateUI(); }, 5000); async function init() { createUI(); if (!authManager.isAuthorized()) { createBanner(); } await loadYearsAndCourses(true); if (authManager.isAuthorized() && state.isLoggedIn) { updateUI(); const banner = document.getElementById('qi-banner'); if (banner) banner.style.display = 'none'; } if (authManager.getStatus() === 'temporary') { addLog(`⏳ 授权剩余 ${authManager.formatRemaining()}`); } updateUI(); setInterval(async () => { try { await fetchStudentInfo(); updateUI(); } catch (_) { state.isLoggedIn = false; updateUI(); } }, 30000); } if (document.readyState === 'complete' || document.readyState === 'interactive') { init(); } else { window.addEventListener('DOMContentLoaded', init); } })();