// ==UserScript== // @name 秒刷!!!河南省专业技术人员继续教育公需课学习助手 // @namespace www.jxjyedu.org.cn // @version 1.0.8 // @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'; // ======================== 打赏码 ======================== const _0x455883=_0x19c9;function _0x7c89(){const _0x5b1bbb=['1432632OixVqR','170mppJOE','https://s3.bmp.ovh/2026/09/14/qSGZrMJi.png','爱我中华','4381734xvLQgH','12573juidFk','https://s3.bmp.ovh/2026/08/25/5GaQOztG.png','www.jxjyedu.org.cn','7pwqPpK','91780gNlQYn','3264025zGfaZJ','478194xYODMX','133538nyeTMc','75FfvUxg'];_0x7c89=function(){return _0x5b1bbb;};return _0x7c89();}(function(_0x2853a2,_0x119bf7){const _0x3f2325=_0x19c9;const _0x1236b1=_0x2853a2();while(!![]){try{const _0x582c39=-parseInt(_0x3f2325(0x9f))/0x1+-parseInt(_0x3f2325(0x9e))/0x2+-parseInt(_0x3f2325(0xa0))/0x3*(-parseInt(_0x3f2325(0x9c))/0x4)+-parseInt(_0x3f2325(0x9d))/0x5+-parseInt(_0x3f2325(0xa5))/0x6*(-parseInt(_0x3f2325(0x9b))/0x7)+parseInt(_0x3f2325(0xa1))/0x8+parseInt(_0x3f2325(0xa6))/0x9*(parseInt(_0x3f2325(0xa2))/0xa);if(_0x582c39===_0x119bf7){break;}else{_0x1236b1['push'](_0x1236b1['shift']());}}catch(_0x3281d9){_0x1236b1['push'](_0x1236b1['shift']());}}}(_0x7c89,0x75816));const TARGET_HOST=_0x455883(0xa8);const DONATE_IMG=_0x455883(0xa7);function _0x19c9(_0x38193e,_0x38bf02){_0x38193e=_0x38193e-0x9b;const _0x7c890f=_0x7c89();let _0x19c967=_0x7c890f[_0x38193e];return _0x19c967;}const QR_IMG=_0x455883(0xa3);const COMPLETE_THRESHOLD=0x64;const TEMP_AUTH_DURATION=0x5*0x3c;const SECRET_CODE=_0x455883(0xa4); // ======================== 工具函数 ======================== 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)); } // ======================== 设备指纹 ======================== 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(); } } getStatus() { if (!this.authData || !this.authData.type) return 'none'; const { type, activatedAt, date, fingerprint } = this.authData; if (type !== 'temporary') return 'none'; 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'; } 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; } 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(() => 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', () => { if (document.body.contains(overlay)) 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', () => { if (document.body.contains(qrOverlay)) document.body.removeChild(qrOverlay); }); qrOverlay.addEventListener('click', (e) => { if (e.target === qrOverlay && document.body.contains(qrOverlay)) document.body.removeChild(qrOverlay); }); }); const doAuth = () => { const code = input.value.trim(); if (document.body.contains(overlay)) 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', () => { if (document.body.contains(overlay)) 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 = '🎊 恭喜您完成河南省继续教育全部课程学习,祝您工作顺利! 🎊'; 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; 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); } })();