this.createCountdownUI(), 500);
this.startLogTimer();
}
}
getStatus() {
if (!this.authData || !this.authData.type) return 'none';
const { type, activatedAt, code, date } = this.authData;
if (type === 'temporary') {
if (activatedAt && (Date.now() - activatedAt) < TEMP_AUTH_DURATION * 1000) return 'temporary';
else { this.clearAuth(); return 'none'; }
} else if (type === 'daily') {
const today = getTodayStr();
if (date === today && DAILY_CODES_MAP[today] && code.toLowerCase() === DAILY_CODES_MAP[today].toLowerCase()) return 'daily';
else { this.clearAuth(); return 'none'; }
} else if (type === 'permanent') {
if (PERMANENT_CODES.some(pc => pc.toLowerCase() === code.toLowerCase())) return 'permanent';
else { this.clearAuth(); return 'none'; }
}
return 'none';
}
activate(code) {
const raw = code.trim().toUpperCase();
const today = getTodayStr();
const tempLower = raw.toLowerCase();
if (TEMPORARY_CODES_MAP[today] && tempLower === TEMPORARY_CODES_MAP[today].toLowerCase()) {
this.authData = { type: 'temporary', activatedAt: Date.now(), code: raw, date: today };
GM_setValue(this.storageKey, this.authData);
this.createCountdownUI();
this.startLogTimer();
return { success: true, type: 'temporary' };
}
if (DAILY_CODES_MAP[today] && tempLower === DAILY_CODES_MAP[today].toLowerCase()) {
this.authData = { type: 'daily', activatedAt: Date.now(), code: raw, date: today };
GM_setValue(this.storageKey, this.authData);
return { success: true, type: 'daily' };
}
if (PERMANENT_CODES.some(pc => pc.toLowerCase() === tempLower)) {
this.authData = { type: 'permanent', activatedAt: Date.now(), code: raw, date: today };
GM_setValue(this.storageKey, this.authData);
return { success: true, type: 'permanent' };
}
return { success: false, msg: '无效授权码' };
}
clearAuth() {
GM_deleteValue(this.storageKey);
this.authData = {};
this.removeCountdownUI();
this.stopLogTimer();
}
isAuthorized() { return this.getStatus() !== 'none'; }
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);
`;
const mins = Math.floor(TEMP_AUTH_DURATION / 60);
const secs = TEMP_AUTH_DURATION % 60;
container.innerHTML = `
⏳
${String(mins).padStart(2,'0')}:${String(secs).padStart(2,'0')}
剩余
`;
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';
this.clearAuth();
showModal({
title: '⏰ 临时授权已到期',
text: '请重新获取授权码',
buttons: [{ text: '确定', primary: true }]
}).then(() => location.reload());
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;
}
// ======================== 第二部分续接(以下内容接第二部分) ========================
// 注意:第二部分将包含所有工具函数、API、刷课逻辑、UI 和初始化。
// 请继续复制第二部分。
// ======================== 3. 核心刷课逻辑 ========================
// ---------- 工具函数 ----------
const sleep = ms => 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));
}
// ---------- 状态 ----------
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,
};
// ---------- API ----------
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();
}
// ---------- 章节并行学习 ----------
async function _soc(chapter, course, freeMode = false) {
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) {
if (freeMode && state.totalProgress >= FREE_THRESHOLD) {
addLog(`免费体验已达 ${FREE_THRESHOLD}%,停止学习`);
state.stopFlag = true;
return false;
}
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();
if (freeMode && state.totalProgress >= FREE_THRESHOLD) {
addLog(`免费体验达到 ${FREE_THRESHOLD}%,停止`);
state.stopFlag = true;
return false;
}
} 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, freeMode = false) {
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, freeMode); } 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 _rs(freeMode = false) {
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('没有可学习的课程');
return 'no_courses';
}
addLog(`开始并发学习 · ${courses.length} 门课 ${freeMode ? '(免费体验模式)' : ''}`);
await Promise.allSettled(courses.map(async course => {
if (state.stopFlag) return;
await _scs(course, freeMode);
}));
if (state.stopFlag) return 'stopped';
updateTotalProgress();
return 'completed';
}
async function freeExperience() {
if (state.running) return;
if (!state.isLoggedIn) {
showModal({ title: '未登录', text: '请先登录平台再体验', buttons: [{ text: '确定', primary: true }] });
return;
}
if (state.totalProgress >= FREE_THRESHOLD) {
showFreeCompleteDialog();
return;
}
state.running = true;
state.stopFlag = false;
document.getElementById('qi-start-btn').textContent = '体验中';
try {
await _rs(true);
updateTotalProgress();
if (state.totalProgress >= FREE_THRESHOLD || state.stopFlag) {
localStorage.setItem('qi_free_complete', 'true');
location.reload();
return;
} else {
addLog(`免费体验结束,当前进度${state.totalProgress}%`);
localStorage.setItem('qi_free_complete', 'true');
location.reload();
}
} catch (e) {
addLog(`体验出错:${e.message}`);
} finally {
state.running = false;
document.getElementById('qi-start-btn').textContent = '开始学习';
if (!state.authManager.isAuthorized()) {
document.getElementById('qi-start-btn').style.opacity = '0.5';
document.getElementById('qi-start-btn').style.pointerEvents = 'none';
}
}
}
async function startFullLearn() {
if (state.running) return;
if (!state.authManager.isAuthorized()) {
addLog('未授权,请先获取授权');
return;
}
if (!state.isLoggedIn) {
showModal({ title: '未登录', text: '请先登录平台', buttons: [{ text: '确定', primary: true }] });
return;
}
state.running = true;
state.stopFlag = false;
document.getElementById('qi-start-btn').textContent = '学习中';
try {
const result = await _rs(false);
if (result === 'completed' && !state.stopFlag) {
addLog('全部课程已学完');
updateTotalProgress();
if (state.totalProgress >= COMPLETE_THRESHOLD) {
showCompletionDialog();
} else {
showModal({ title: '学习完成', text: `总进度 ${state.totalProgress}%,请检查网络或继续学习剩余课程`, buttons: [{ text: '确定', primary: true }] });
}
}
} catch (e) {
addLog(`学习出错:${e.message}`);
} finally {
state.running = false;
document.getElementById('qi-start-btn').textContent = '开始学习';
if (!state.authManager.isAuthorized()) {
document.getElementById('qi-start-btn').style.opacity = '0.5';
document.getElementById('qi-start-btn').style.pointerEvents = 'none';
}
}
}
// ======================== 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 showFreeCompleteDialog() {
showModal({
title: '🎉 免费体验已结束',
html: `
您已完成 30% 的课程学习,如需继续完成全部课程,请获取授权码。
关注公众号“叙言哥哥”发送“河南专技”获取授权码。
`,
buttons: []
});
document.addEventListener('click', function handler(e) {
if (e.target && e.target.id === 'dialog-auth-btn') {
e.stopPropagation();
const overlay = document.getElementById('jxjy-modal-overlay');
if (overlay) document.body.removeChild(overlay);
requestAuthorization();
document.removeEventListener('click', handler);
}
});
}
function requestAuthorization() {
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 }] });
return;
}
const result = state.authManager.activate(code);
if (result.success) {
let msg = '';
if (result.type === 'temporary') msg = '⏳ 5分钟临时授权已生效,右上角显示倒计时,日志每30秒刷新剩余时间。';
else if (result.type === 'daily') msg = '🎉 今日授权已生效,今日内有效。';
else if (result.type === 'permanent') msg = '🔓 永久授权已生效,感谢支持!';
// 写入日志
addLog(`✅ 授权成功:${result.type === 'temporary' ? '临时' : result.type === 'daily' ? '延时' : '永久'}授权`);
showModal({
title: '✅ 授权成功',
text: msg,
buttons: [{ text: '知道了', primary: true }]
}).then(() => {
updateUI();
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();
});
}
function showCompletionDialog() {
if (state.completionShown) return;
state.completionShown = true;
const marqueeText = '🎊 恭喜您完成 2026 年河南省继续教育全部课程学习,祝您工作顺利! 🎊';
showModal({
title: '🎉 学习完成!',
html: `
您已按顺序完成全部课程的学习任务,获得相应学时。
本次学习圆满结束,祝您假期愉快!
`,
buttons: []
});
document.addEventListener('click', function handler(e) {
if (e.target && e.target.id === 'thanks-btn') {
e.stopPropagation();
showModal({
title: '💖 感谢支持',
html: `
`,
buttons: [{ text: '关闭', primary: true }]
});
const overlay = document.getElementById('jxjy-modal-overlay');
if (overlay) document.body.removeChild(overlay);
document.removeEventListener('click', handler);
}
});
}
function showGuide() {
showModal({
title: '📚 使用指南',
html: `
🚀 免费体验流程
1
登录平台:确保已登录河南省继续教育学会账号。
2
点击 “免费体验”(顶部横幅按钮),脚本将自动学习所有课程至总进度 30% 后停止,并刷新页面弹出授权提示。
3
获取授权码(见下方)后输入,解锁“开始学习”按钮,完成全部课程。
🔑 授权码说明
- 临时授权:5分钟有效,适合临时刷课,右上角显示倒计时,日志每30秒刷新剩余时间。
- 延时授权:当日全天有效。
- 永久授权:永久有效。
- 关注公众号 “叙言哥哥” 发送 “河南专技” 获取对应授权码。
💡 其他功能
- 打赏:点击左侧粉色按钮,扫描二维码支持作者。
- 日志:右下角实时显示学习进度和状态(登录/未登录、课程进度等)。
`,
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 = `
⚠️ 未授权 — 您可以免费学习 30% 的课程,或获取授权码解锁全部。
`;
document.body.appendChild(banner);
document.getElementById('qi-free-btn').addEventListener('click', () => {
if (state.authManager.isAuthorized()) {
showModal({ title: '已授权', text: '您已获得授权,可直接使用“开始学习”完成全部课程', buttons: [{ text: '确定', primary: true }] });
return;
}
if (!state.isLoggedIn) {
showModal({ title: '未登录', text: '请先登录平台再体验', buttons: [{ text: '确定', primary: true }] });
return;
}
freeExperience();
});
document.getElementById('qi-auth-btn').addEventListener('click', () => {
requestAuthorization();
});
}
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: '请先获取授权码,或点击“免费体验”学习30%', buttons: [{ text: '知道了', primary: true }] });
}
}
}},
{ id: 'qi-donate-btn', text: '💖
打赏', click: () => {
showModal({
title: '💖 感谢打赏',
html: `
`,
buttons: [{ text: '关闭', primary: true }]
});
}}
];
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;
if (localStorage.getItem('qi_free_complete') === 'true') {
localStorage.removeItem('qi_free_complete');
setTimeout(() => showFreeCompleteDialog(), 800);
}
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);
}
})();