// ==UserScript==
// @name 安徽专业技术继续教育公需一学习助手
// @version 1.3.0
// @description 支持安徽专业技术继续教育公需一自动学习,免费1次体验,6.8元/3次超值刷课,激活后可帮同事刷3次(1次=1个账号全部课程+考试),全程后台静默运行,支持效率模拟人工倍速/1:1慢刷/狂暴极速3种模式,考试时长可调
// @author berain
// @match https://www.zjzx.ah.cn/*
// @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png
// @grant GM_openInTab
// @grant GM_setClipboard
// @antifeature payment
// @antifeature membership
// @antifeature tracking
// @tag 安徽专业技术继续教育
// @tag 安徽继续教育公需一
// @tag 6.8元刷3次
// @tag 自动学习
// @tag 课程辅助
// @tag 考试辅助
// @tag 继续教育
// @run-at document-end
// @license LGPL-3.0
// ==/UserScript==
(function() {
'use strict';
const API_BASE = 'https://1300815573-7g7cz1ts5w.ap-shanghai.tencentscf.com';
const QQ_GROUP_NUMBER = '1041375393';
const AUTHOR_QQ = '3352639207';
const QQ_GROUP_LINK = 'https://qm.qq.com/q/EBLDisQRBm';
const AUTHOR_ADD_LINK = 'https://qm.qq.com/q/C9M2GTbN5K';
const STORAGE_KEY_PANEL_POS = 'zjzx_panel_pos_v1';
const FREE_TRIAL_KEY = 'zjzx_free_trial_used';
let state = {
taskId: null,
running: false,
courses: [],
selected: [],
logLines: [],
cloudAuthorized: false,
cloudRemaining: 0,
cloudChecked: false,
isLoggedIn: false,
mode: 'efficiency',
multiplier: 3,
examDuration: 30,
pollTimer: null,
activeTab: 'course',
currentCsId: null,
videoCache: new Map(),
refreshCounter: 0,
lastCourseUpdate: 0,
modalVisible: false,
};
let displayedSet = new Set();
// ================================================================
// ★★★ 日志上报函数 ★★★
// ================================================================
async function reportUsage(event, detail) {
try {
const custId = getCustId();
if (!custId) return;
await fetch(`${API_BASE}/report`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ custId, event, detail: detail || {} })
});
} catch (_) {}
}
function getCookie(name) {
const m = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
return m ? decodeURIComponent(m[1]) : '';
}
function getUserData() {
try {
const raw = getCookie('userData_lenye') || localStorage.getItem('userData_lenye');
if (raw) return JSON.parse(raw);
} catch (_) {}
return null;
}
function getCustId() {
try {
const raw = getCookie('userData_lenye') || localStorage.getItem('userData_lenye');
if (raw) {
const data = JSON.parse(raw);
return data.custId || '';
}
} catch (_) {}
return '';
}
function getSessionToken() {
const user = getUserData();
return getCookie('SESSION_TOKEN') || user?.SESSION_TOKEN || '';
}
function getUUID() {
const user = getUserData();
let uuid = getCookie('UUID') || user?.UUID || '';
if (!uuid && crypto.randomUUID) uuid = crypto.randomUUID();
return uuid;
}
function checkIsLoggedIn() {
return !!(getCookie('SESSION_TOKEN') || getUserData()?.SESSION_TOKEN);
}
function log(msg) {
const ts = new Date().toLocaleTimeString('zh-CN', { hour12: false });
const line = `${ts} ${msg}`;
displayedSet.add(msg);
state.logLines.push(line);
if (state.logLines.length > 500) {
state.logLines = state.logLines.slice(-500);
}
console.log(line);
updateLogUI();
updateUI();
}
function getModeLabel(mode) {
const map = { 'realtime': '🐢 1:1慢刷', 'efficiency': '⚡ 效率', 'berserk': '💥 狂暴' };
return map[mode] || mode;
}
function isUnfinished(c) {
const p = parseFloat(c.lcsProcess || 0);
const hasExam = c.courseTestList && c.courseTestList.length > 0;
const examDone = c.lcsExameFinished === '1';
return p < 100 || (hasExam && !examDone);
}
function getCourseStatus(c) {
const p = parseFloat(c.lcsProcess || 0);
const hasExam = c.courseTestList && c.courseTestList.length > 0;
const examDone = c.lcsExameFinished === '1';
if (p < 100) return '⏳ 未学完';
if (hasExam && !examDone) return '📝 待考试';
return '✅ 已完成';
}
async function grantFreeTrial(custId) {
try {
const res = await fetch(`${API_BASE}/free`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ custId })
});
const data = await res.json();
if (data.success) {
state.cloudAuthorized = true;
state.cloudRemaining = data.remaining || 1;
state.cloudChecked = true;
localStorage.setItem(FREE_TRIAL_KEY, '1');
log('🎁 首次免费体验已激活,剩余 1 次');
// ★★★ 上报:免费体验 ★★★
await reportUsage('free_trial', { source: 'auto_grant' });
updateUI();
return true;
} else {
log(`⚠️ 免费体验发放失败: ${data.message || '未知错误'}`);
return false;
}
} catch (e) {
log(`⚠️ 免费体验请求失败: ${e.message}`);
return false;
}
}
async function callAPI(endpoint, payload = {}) {
const custId = getCustId();
const sessionToken = getSessionToken();
const uuid = getUUID();
if (!custId) throw new Error('未登录,请刷新页面重试');
const body = JSON.stringify({ custId, sessionToken, uuid, ...payload });
const res = await fetch(`${API_BASE}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
}
async function fetchVideos(csId) {
if (state.videoCache.has(csId)) {
return state.videoCache.get(csId);
}
try {
const res = await callAPI('/videos', { csId });
const videos = res.data || [];
state.videoCache.set(csId, videos);
return videos;
} catch (e) {
console.error('获取视频列表失败:', e);
return null;
}
}
async function checkAuth() {
const custId = getCustId();
state.isLoggedIn = checkIsLoggedIn();
if (!state.isLoggedIn || !custId) {
state.cloudChecked = true;
state.cloudAuthorized = false;
state.cloudRemaining = 0;
state.isLoggedIn = false;
updateUI();
return false;
}
try {
const res = await fetch(`${API_BASE}/check?cust_id=${encodeURIComponent(custId)}`);
const data = await res.json();
state.cloudAuthorized = data.authorized || false;
state.cloudRemaining = data.remaining || 0;
state.cloudChecked = true;
const freeUsed = localStorage.getItem(FREE_TRIAL_KEY) === '1';
if (!state.cloudAuthorized && state.cloudRemaining === 0 && !freeUsed) {
const granted = await grantFreeTrial(custId);
if (granted) {
const res2 = await fetch(`${API_BASE}/check?cust_id=${encodeURIComponent(custId)}`);
const data2 = await res2.json();
state.cloudAuthorized = data2.authorized || false;
state.cloudRemaining = data2.remaining || 0;
state.cloudChecked = true;
}
}
const freeUsedNow = localStorage.getItem(FREE_TRIAL_KEY) === '1';
if (state.cloudRemaining === 0 && !state.cloudAuthorized && freeUsedNow && !state.modalVisible) {
setTimeout(() => {
if (state.cloudRemaining === 0 && !state.cloudAuthorized && !state.modalVisible) {
showPurchaseModal();
}
}, 2000);
}
if (state.cloudRemaining > 0 && state.modalVisible) {
closePurchaseModal();
}
state.lastRemaining = state.cloudRemaining;
return state.cloudAuthorized || state.cloudRemaining > 0;
} catch (e) {
state.cloudChecked = true;
state.cloudAuthorized = false;
state.cloudRemaining = 0;
return false;
}
}
async function activateKey(key) {
const custId = getCustId();
if (!custId) return { success: false, message: '请先登录' };
const res = await fetch(`${API_BASE}/activate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ custId, key })
});
const result = await res.json();
if (result.success) {
await checkAuth();
if (state.cloudRemaining > 0) {
closePurchaseModal();
}
// ★★★ 上报:密钥激活 ★★★
await reportUsage('activate', { key: key, success: true });
}
return result;
}
async function fetchCourses() {
const res = await callAPI('/courses', {});
return res.data || [];
}
async function startTask(courseIds) {
const res = await callAPI('/start', {
courseIds,
mode: state.mode,
multiplier: state.multiplier,
examDuration: state.examDuration
});
if (!res.success) throw new Error(res.message);
state.taskId = res.taskId;
return res.taskId;
}
async function getTaskStatus() {
if (!state.taskId) return null;
const res = await callAPI('/status', { taskId: state.taskId });
return res;
}
async function stopTask() {
if (!state.taskId) return;
await callAPI('/stop', { taskId: state.taskId });
}
function showPurchaseModal() {
if (document.getElementById('zjzx-purchase-modal-overlay')) {
state.modalVisible = true;
return;
}
state.modalVisible = true;
const overlay = document.createElement('div');
overlay.id = 'zjzx-purchase-modal-overlay';
overlay.style.cssText = `
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
z-index: 99999999;
background: rgba(0,0,0,0.7);
display: flex; justify-content: center; align-items: center;
backdrop-filter: blur(8px);
animation: zjzxModalFadeIn 0.4s ease;
`;
const modal = document.createElement('div');
modal.style.cssText = `
background: linear-gradient(145deg, #ffffff, #fef3c7);
border-radius: 32px;
padding: 40px 48px 32px;
max-width: 480px;
width: 92%;
box-shadow: 0 30px 80px rgba(180,83,9,0.5);
text-align: center;
border: 4px solid #f59e0b;
position: relative;
animation: zjzxModalBounce 0.5s ease;
`;
modal.innerHTML = `
🔒
次数已用完!
请购买密钥后继续使用
💬 加入QQ群获取帮助
📢 ${QQ_GROUP_NUMBER}
👤 直接添加作者QQ好友
QQ: ${AUTHOR_QQ}
关闭后自动跳转到「激活授权」面板
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
document.getElementById('zjzx-modal-join-group').addEventListener('click', function(e) {
e.stopPropagation();
try {
if (typeof GM_openInTab === 'function') {
GM_openInTab(QQ_GROUP_LINK, { active: true, insert: true, setParent: true });
} else {
window.open(QQ_GROUP_LINK, '_blank');
}
} catch (_) { window.open(QQ_GROUP_LINK, '_blank'); }
});
document.getElementById('zjzx-modal-add-author').addEventListener('click', function(e) {
e.stopPropagation();
try {
if (typeof GM_openInTab === 'function') {
GM_openInTab(AUTHOR_ADD_LINK, { active: true, insert: true, setParent: true });
} else {
window.open(AUTHOR_ADD_LINK, '_blank');
}
} catch (_) { window.open(AUTHOR_ADD_LINK, '_blank'); }
});
document.getElementById('zjzx-modal-close').addEventListener('click', function(e) {
e.stopPropagation();
closePurchaseModal();
switchTab('auth');
setTimeout(() => {
const input = document.getElementById('zjzx-license-key');
if (input) {
input.focus();
input.select();
input.style.borderColor = '#f59e0b';
input.style.boxShadow = '0 0 0 3px rgba(245,158,11,0.3)';
}
}, 300);
});
overlay.addEventListener('click', function(e) {
if (e.target === overlay) {
closePurchaseModal();
}
});
if (!document.getElementById('zjzx-modal-styles')) {
const style = document.createElement('style');
style.id = 'zjzx-modal-styles';
style.textContent = `
@keyframes zjzxModalFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes zjzxModalBounce { 0% { transform: scale(0.7) translateY(30px); opacity: 0; } 60% { transform: scale(1.03) translateY(-5px); opacity: 1; } 100% { transform: scale(1) translateY(0); } }
#zjzx-modal-join-group:hover { transform: scale(1.03); box-shadow: 0 6px 24px rgba(59,130,246,0.5); }
#zjzx-modal-add-author:hover { transform: scale(1.03); box-shadow: 0 6px 24px rgba(245,158,11,0.5); }
#zjzx-modal-close:hover { background: #fef3c7; border-color: #f59e0b; transform: scale(1.02); }
`;
document.head.appendChild(style);
}
state.modalVisible = true;
}
function closePurchaseModal() {
const overlay = document.getElementById('zjzx-purchase-modal-overlay');
if (overlay) {
overlay.remove();
}
state.modalVisible = false;
}
function updateUI() {
const startBtn = document.getElementById('zjzx-start-btn');
const stopBtn = document.getElementById('zjzx-stop-btn');
const dot = document.getElementById('zjzx-status-dot');
const text = document.getElementById('zjzx-status-text');
const isLoggedIn = state.isLoggedIn;
const canStart = isLoggedIn && state.cloudAuthorized && state.cloudRemaining > 0 && !state.running;
if (startBtn) {
startBtn.disabled = !canStart;
startBtn.textContent = state.running ? '🏃 运行中' : '▶ 开始';
startBtn.style.opacity = canStart ? '1' : '0.6';
startBtn.title = !isLoggedIn ? '请先登录平台' : (!state.cloudAuthorized || state.cloudRemaining <= 0 ? '次数已用完,请购买密钥' : '');
}
if (stopBtn) {
stopBtn.disabled = !state.running;
stopBtn.textContent = '⏹ 停止';
}
if (dot) dot.className = state.running ? 'zjzx-dot-green' : 'zjzx-dot-gray';
if (text) {
if (!isLoggedIn) {
text.textContent = '🔓 免费体验1次(登录后使用)';
text.style.color = '#f59e0b';
} else if (!state.cloudChecked) {
text.textContent = '⏳ 授权检查中...';
text.style.color = '#f59e0b';
} else if (!state.cloudAuthorized || state.cloudRemaining <= 0) {
text.textContent = '🔒 次数已用完 (联系作者购买)';
text.style.color = '#dc2626';
} else {
text.textContent = state.running ? '🏃 运行中...' : '⏸ 已停止';
text.style.color = '#1e293b';
}
}
if (state.activeTab === 'course') renderCourseList();
updateQueueSummary();
updateCurrentInfo();
updateAuthUI();
updateMiniAuth();
}
function updateLogUI() {
const box = document.getElementById('zjzx-run-log');
if (box) {
const lines = state.logLines.slice(-100);
box.innerHTML = lines.length ? lines.map(line => `${line}
`).join('') : '⏳ 等待日志输出...
';
box.scrollTop = box.scrollHeight;
}
const realtimeBox = document.getElementById('zjzx-realtime-log');
if (realtimeBox) {
const lines = state.logLines.slice(-8);
realtimeBox.innerHTML = lines.length ? lines.map(line => `${line}
`).join('') : '⏳ 等待日志...
';
realtimeBox.scrollTop = realtimeBox.scrollHeight;
}
updateAuthUI();
updateMiniAuth();
}
function updateAuthUI() {
const authStatus = document.getElementById('zjzx-auth-status');
const authRemaining = document.getElementById('zjzx-auth-remaining');
const authTip = document.getElementById('zjzx-auth-tip');
if (authStatus) {
let statusText = '';
let color = '';
if (!state.isLoggedIn) {
statusText = '✅ 可用 (1次免费)';
color = '#f59e0b';
} else if (!state.cloudChecked) {
statusText = '⏳ 检查中...';
color = '#f59e0b';
} else if (state.cloudAuthorized && state.cloudRemaining > 0) {
statusText = `✅ 可用 (${state.cloudRemaining}次)`;
color = '#065f46';
} else {
statusText = '🔒 次数已用完 (联系作者购买)';
color = '#dc2626';
}
authStatus.textContent = statusText;
authStatus.style.color = color;
}
if (authRemaining) {
if (!state.isLoggedIn) {
authRemaining.textContent = '1(登录后可用)';
authRemaining.style.color = '#f59e0b';
} else {
authRemaining.textContent = String(state.cloudRemaining);
authRemaining.style.color = state.cloudRemaining > 0 ? '#065f46' : '#dc2626';
}
}
if (authTip) {
if (!state.cloudChecked) {
authTip.textContent = '';
} else {
authTip.textContent = '如有问题加入群聊或加 QQ:' + AUTHOR_QQ;
authTip.style.textAlign = 'center';
authTip.style.fontSize = '14px';
authTip.style.fontWeight = 'bold';
authTip.style.color = '#92400e';
}
}
}
// ★★★ 迷你授权状态(联系信息加括号) ★★★
function updateMiniAuth() {
const mini = document.getElementById('zjzx-mini-auth');
if (!mini) return;
let mainText = '';
let color = '#92400e';
const contactText = '(如有问题加入群聊或加 QQ:' + AUTHOR_QQ + ')';
if (!state.isLoggedIn) {
mainText = '🔑 授权状态:✅ 可用 (1次免费) ' + contactText;
color = '#f59e0b';
} else if (!state.cloudChecked) {
mainText = '⏳ 授权检查中...';
color = '#f59e0b';
} else if (state.cloudAuthorized && state.cloudRemaining > 0) {
mainText = '🔑 授权状态:✅ 可用 (' + state.cloudRemaining + '次) ' + contactText;
color = '#065f46';
} else {
mainText = '🔑 授权状态:🔒 次数已用完 ' + contactText;
color = '#dc2626';
}
mini.textContent = mainText;
mini.style.color = color;
}
function updateQueueSummary() {
const doneEl = document.getElementById('zjzx-queue-done');
const totalEl = document.getElementById('zjzx-queue-total');
const pctEl = document.getElementById('zjzx-queue-percent');
const barEl = document.getElementById('zjzx-queue-progress');
const examEl = document.getElementById('zjzx-queue-exam');
const total = state.courses.length;
const done = state.courses.filter(c => parseFloat(c.lcsProcess || 0) >= 100).length;
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
if (doneEl) doneEl.textContent = String(done);
if (totalEl) totalEl.textContent = String(total);
if (pctEl) pctEl.textContent = `${pct}%`;
if (barEl) barEl.style.width = `${pct}%`;
if (examEl) {
const examTotal = state.courses.filter(c => c.courseTestList && c.courseTestList.length > 0).length;
const examDone = state.courses.filter(c => c.lcsExameFinished === '1').length;
examEl.textContent = examTotal > 0 ? `${examDone} / ${examTotal}` : '—';
}
}
function updateCurrentInfo() {
const courseEl = document.getElementById('zjzx-current-course');
const chapterEl = document.getElementById('zjzx-current-chapter');
const taskEl = document.getElementById('zjzx-current-task');
if (!courseEl) return;
const currentCourse = state.courses.find(c => c.csId === state.currentCsId);
courseEl.textContent = currentCourse?.csName || '无';
chapterEl.textContent = getModeLabel(state.mode);
if (state.running) taskEl.textContent = '🏃 运行中...';
else {
const selectedCount = state.selected.length;
taskEl.textContent = selectedCount > 0 ? `📚 已选 ${selectedCount} 门课程` : '👉 点开始后自动提交';
}
}
function renderCourseList() {
const list = document.getElementById('zjzx-course-list');
if (!list) return;
const sortedCourses = [...state.courses].sort((a, b) => {
const aUnfinished = isUnfinished(a) ? 0 : 1;
const bUnfinished = isUnfinished(b) ? 0 : 1;
return aUnfinished - bUnfinished;
});
if (!sortedCourses.length) {
list.innerHTML = `📭 当前年度计划暂无课程
可切换上方培训计划查看其他年度
`;
return;
}
list.innerHTML = sortedCourses.map(c => {
const pct = parseFloat(c.lcsProcess || 0).toFixed(1);
const checked = state.selected.includes(c.csId) ? 'checked' : '';
const statusText = getCourseStatus(c);
let statusClass = '';
let statusIcon = '';
if (statusText.includes('待考试')) { statusClass = 'exam'; statusIcon = '📝'; }
else if (statusText.includes('未学完')) { statusClass = 'learning'; statusIcon = '⏳'; }
else { statusClass = 'done'; statusIcon = '✅'; }
return ``;
}).join('');
list.querySelectorAll('input[type=checkbox]').forEach(el => {
el.addEventListener('change', function() {
const id = this.dataset.csid;
if (this.checked) {
if (!state.selected.includes(id)) state.selected.push(id);
} else {
state.selected = state.selected.filter(s => s !== id);
}
updateUI();
});
});
}
async function renderChapterPreview() {
const box = document.getElementById('zjzx-chapter-preview');
if (!box) return;
let targetCourses = state.courses.filter(c => state.selected.includes(c.csId));
if (!targetCourses.length) {
targetCourses = state.courses.filter(c => isUnfinished(c));
}
if (!targetCourses.length) {
box.innerHTML = '📖 请先选择课程,或当前无未完成课程
';
return;
}
let html = '';
for (const course of targetCourses) {
try {
state.videoCache.delete(course.csId);
const videos = await fetchVideos(course.csId);
const statusText = getCourseStatus(course);
const pct = parseFloat(course.lcsProcess || 0).toFixed(1);
if (videos === null) {
html += `
📺 ${course.csName} 获取失败
⚠️ 获取视频列表失败,请重试
`;
continue;
}
if (videos.length === 0) {
const hasExam = course.courseTestList && course.courseTestList.length > 0;
const examDone = course.lcsExameFinished === '1';
let extraInfo = '📭 无视频';
if (hasExam && !examDone) extraInfo += '(仅考试,待完成)';
else if (hasExam && examDone) extraInfo += '(仅考试,已通过 ✅)';
else extraInfo += '(仅考试)';
html += `
📺 ${course.csName}
${extraInfo}
${statusText} ${pct}%
${hasExam ? (examDone ? '✅ 考试已通过' : '📝 待考试') : '无考试'}
`;
} else {
const items = videos.map(v => {
const done = v.studyFinished === '1' || v.process >= 100;
const mark = done ? '✅' : '⏳';
const pct2 = Math.round(v.process || 0);
const name = v.resName || v.resId || '未命名视频';
const displayName = name.length > 25 ? name.slice(0, 22) + '...' : name;
return `
${mark} ${displayName}
${done ? '✅' : pct2 + '%'}
`;
}).join('');
const hasExam = course.courseTestList && course.courseTestList.length > 0;
const examDone = course.lcsExameFinished === '1';
let examInfo = '';
if (hasExam) {
examInfo = examDone ? '📝 考试已通过 ✅' : '📝 待考试';
}
html += `
📺 ${course.csName}
${statusText} ${pct}% ${examInfo ? '| ' + examInfo : ''}
${items}
`;
}
} catch (e) {
html += `
❌ ${course.csName}
加载出错: ${e.message}
`;
}
}
box.innerHTML = html || '📭 暂无课程数据
';
}
function switchTab(tab) {
state.activeTab = tab;
document.querySelectorAll('.zjzx-tab-btn').forEach(el => {
el.classList.toggle('active', el.dataset.tab === tab);
});
document.querySelectorAll('.zjzx-pane').forEach(el => {
el.classList.toggle('active', el.dataset.pane === tab);
});
const realtimeArea = document.getElementById('zjzx-realtime-area');
if (realtimeArea) realtimeArea.style.display = (tab === 'course') ? '' : 'none';
if (tab === 'log') updateLogUI();
if (tab === 'chapter') renderChapterPreview();
}
let refreshingCourses = false;
async function refreshCourseListData(force = false) {
if (refreshingCourses) return;
const now = Date.now();
if (!force && (now - state.lastCourseUpdate) < 3000) return;
refreshingCourses = true;
try {
const courses = await fetchCourses();
const oldSelected = state.selected;
state.courses = courses;
const currentIds = new Set(courses.map(c => c.csId));
state.selected = oldSelected.filter(id => currentIds.has(id));
state.lastCourseUpdate = now;
state.videoCache.clear();
renderCourseList();
updateUI();
updateQueueSummary();
} catch (e) {
console.warn('刷新课程列表失败:', e.message);
} finally {
refreshingCourses = false;
}
}
async function autoSelectUnfinished() {
await refreshCourseListData(true);
const unfinishedIds = state.courses.filter(c => isUnfinished(c)).map(c => c.csId);
state.selected = unfinishedIds;
renderCourseList();
updateUI();
updateQueueSummary();
if (unfinishedIds.length > 0) {
log(`🎯 已自动勾选 ${unfinishedIds.length} 门未完成课程`);
} else {
log('🎯 所有课程已完成,无需勾选');
}
}
async function startStudy() {
if (state.running) return;
if (!state.cloudAuthorized || state.cloudRemaining <= 0) {
log('🔒 授权次数已用完,请激活或联系作者购买');
setTimeout(() => showPurchaseModal(), 500);
updateUI();
return;
}
if (!state.selected.length) {
log('⚠️ 请勾选课程');
return;
}
try {
await refreshCourseListData(true);
const taskId = await startTask(state.selected);
state.running = true;
state.taskId = taskId;
displayedSet = new Set();
state.refreshCounter = 0;
updateUI();
log(`🚀 任务已启动,ID: ${taskId}`);
// ★★★ 上报:启动任务 ★★★
await reportUsage('start_task', {
course_count: state.selected.length,
mode: state.mode,
multiplier: state.multiplier
});
if (state.pollTimer) clearInterval(state.pollTimer);
state.pollTimer = setInterval(async () => {
try {
const status = await getTaskStatus();
if (!status) return;
if (status.logs && status.logs.length) {
status.logs.forEach(msg => {
if (!displayedSet.has(msg)) {
log(msg);
}
});
}
state.refreshCounter++;
if (state.refreshCounter % 2 === 0) {
await refreshCourseListData(false);
}
if (status.status === 'completed') {
state.running = false;
clearInterval(state.pollTimer);
state.pollTimer = null;
log('🎉 所有课程学习完成!');
await refreshCourseListData(true);
updateUI();
// ★★★ 上报:消耗次数 + 完成任务 ★★★
const useResult = await callAPI('/use', {});
await reportUsage('use', { remaining: useResult.remaining || 0 });
await reportUsage('complete_task', {
courses_done: status.courseIndex || 0,
progress: status.progress || 100
});
await checkAuth();
updateUI();
} else if (status.status === 'stopped') {
state.running = false;
clearInterval(state.pollTimer);
state.pollTimer = null;
log('⏹ 已停止');
await refreshCourseListData(true);
updateUI();
} else if (status.status === 'error') {
state.running = false;
clearInterval(state.pollTimer);
state.pollTimer = null;
log('❌ 任务出错,请查看日志');
await refreshCourseListData(true);
updateUI();
}
updateUI();
} catch (e) {
console.warn('轮询状态失败:', e.message);
}
}, 3000);
} catch (e) {
log('❌ 启动失败:' + e.message);
}
}
async function stopStudy() {
if (!state.running) return;
try {
await stopTask();
state.running = false;
if (state.pollTimer) {
clearInterval(state.pollTimer);
state.pollTimer = null;
}
log('⏹ 已发送停止指令');
await refreshCourseListData(true);
updateUI();
} catch (e) {
log('❌ 停止失败:' + e.message);
}
}
function createUI() {
if (document.getElementById('free-zjzx-panel')) return;
const style = document.createElement('style');
style.textContent = `
#free-zjzx-panel {
position: fixed; right: 20px; top: 80px; z-index: 999999;
width: 420px; max-height: calc(100vh - 120px);
background: #fffbf5; border-radius: 16px;
box-shadow: 0 8px 32px rgba(180,83,9,0.18);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 13px; color: #1e293b;
display: flex; flex-direction: column;
border: 2px solid #fcd34d; overflow: hidden;
}
.zjzx-panel-header {
background: linear-gradient(135deg, #f59e0b, #f97316);
color: #fff; padding: 10px 16px;
cursor: move; user-select: none;
display: flex; justify-content: space-between; align-items: center;
flex-shrink: 0;
}
.zjzx-panel-header .zjzx-title { font-size: 15px; font-weight: 800; }
.zjzx-panel-header .zjzx-close-btn {
background: rgba(255,255,255,0.2); border: none; color: #fff;
font-size: 18px; cursor: pointer; padding: 0 8px; border-radius: 6px;
}
.zjzx-status-card {
background: #fffbeb; margin: 6px 10px 4px; padding: 6px 10px;
border-radius: 10px; border: 2px solid #fde68a; flex-shrink: 0;
}
.zjzx-status-row { display: flex; justify-content: space-between; }
.zjzx-status-label { font-size: 11px; color: #92400e; font-weight: 700; }
.zjzx-status-badge {
font-size: 11px; font-weight: 700; color: #78350f;
background: #fff; padding: 1px 10px; border-radius: 20px;
border: 1px solid #fcd34d;
}
.zjzx-status-main {
display: flex; justify-content: space-between; align-items: center;
margin-top: 2px;
}
.zjzx-status-metrics {
display: flex; align-items: center; gap: 4px;
font-size: 12px; color: #78350f;
}
.zjzx-status-metrics em { font-style: normal; font-weight: 800; color: #92400e; }
.zjzx-progress-pct { font-size: 16px; font-weight: 800; color: #f97316; }
.zjzx-progress-bar {
height: 5px; background: #fef3c7; border-radius: 4px;
margin-top: 3px; overflow: hidden; border: 1px solid #fde68a;
}
.zjzx-progress-bar span {
display: block; height: 100%;
background: linear-gradient(90deg, #f59e0b, #f97316);
border-radius: 4px; transition: width 0.4s ease; width: 0%;
}
.zjzx-mini-auth {
font-size: 10px; color: #92400e; text-align: center;
margin-top: 3px; font-weight: 600;
}
.zjzx-controls {
display: flex; gap: 6px; margin: 3px 10px 5px; padding: 6px 10px;
flex-wrap: wrap; align-items: center;
background: #fffbeb; border-radius: 10px;
border: 2px solid #fde68a; flex-shrink: 0;
}
.zjzx-controls select {
padding: 5px 8px; border-radius: 8px;
border: 2px solid #fde68a; background: #fff;
font-size: 12px; font-weight: 700; color: #92400e;
flex: 1.2; min-width: 80px; height: 32px;
}
.zjzx-controls input[type="number"] {
width: 46px; padding: 4px 4px; border-radius: 8px;
border: 2px solid #fde68a; background: #fff;
font-size: 13px; text-align: center; color: #92400e;
font-weight: 700; height: 32px;
}
.zjzx-controls .zjzx-control-label {
font-size: 12px; color: #92400e; font-weight: 700;
}
.zjzx-btn {
padding: 5px 12px; border-radius: 10px; border: none;
font-size: 13px; font-weight: 800; cursor: pointer;
transition: all 0.25s; flex: 1; min-width: 50px; height: 32px;
display: flex; align-items: center; justify-content: center;
}
.zjzx-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none !important; }
.zjzx-btn-start {
background: linear-gradient(135deg, #f59e0b, #f97316);
color: #fff; box-shadow: 0 2px 8px rgba(245,158,11,0.3);
}
.zjzx-btn-start:hover:not(:disabled) {
transform: translateY(-2px) scale(1.02);
box-shadow: 0 4px 16px rgba(245,158,11,0.4);
}
.zjzx-btn-stop {
background: #ef4444; color: #fff;
box-shadow: 0 2px 8px rgba(239,68,68,0.25);
}
.zjzx-btn-stop:hover:not(:disabled) {
transform: translateY(-2px) scale(1.02);
box-shadow: 0 4px 16px rgba(239,68,68,0.35);
}
.zjzx-settings-row {
display: flex;
align-items: center;
gap: 12px;
padding: 3px 10px 5px;
background: #fffbeb;
border-radius: 8px;
border: 1px solid #fde68a;
margin: 2px 10px 4px;
flex-wrap: nowrap;
}
.zjzx-settings-row .exam-group {
display: flex;
align-items: center;
gap: 8px;
flex: 0 1 auto;
}
.zjzx-settings-row .exam-group label {
font-size: 12px;
font-weight: 700;
color: #92400e;
display: flex;
align-items: center;
gap: 4px;
}
.zjzx-settings-row .exam-group input[type="range"] {
flex: 0 0 80px;
min-width: 60px;
accent-color: #f59e0b;
}
.zjzx-settings-row .exam-group .exam-value {
font-size: 13px;
font-weight: 800;
color: #f97316;
min-width: 30px;
text-align: center;
}
.zjzx-settings-row .sync-group {
display: flex;
align-items: center;
gap: 12px;
margin-left: auto;
flex: 0 0 auto;
}
.zjzx-settings-row .sync-group label {
font-size: 12px;
font-weight: 700;
color: #92400e;
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
}
.zjzx-settings-row .sync-group .sync-label {
font-size: 12px;
font-weight: 700;
color: #92400e;
cursor: pointer;
}
.zjzx-settings-row .sync-group .manual-sync {
font-size: 12px;
font-weight: 700;
color: #92400e;
cursor: pointer;
padding: 2px 4px;
user-select: none;
background: transparent;
border: none;
outline: none;
}
.zjzx-settings-row .sync-group .manual-sync:hover {
color: #b45309;
text-decoration: underline;
}
.zjzx-tabbar {
display: flex; gap: 2px; padding: 0 10px; margin-top: 2px;
flex-shrink: 0; border-bottom: 2px solid #fde68a;
}
.zjzx-tab-btn {
flex: 1; border: none; background: transparent; padding: 5px 4px;
font-size: 12px; font-weight: 700; color: #b45309;
cursor: pointer; border-bottom: 3px solid transparent;
transition: all 0.2s; border-radius: 0;
}
.zjzx-tab-btn:hover { color: #78350f; background: #fffbeb; }
.zjzx-tab-btn.active {
color: #92400e; border-bottom-color: #f59e0b;
background: #fffbeb;
}
.zjzx-panel-body {
flex: 1; overflow-y: auto; padding: 4px 10px 2px; min-height: 0;
}
.zjzx-pane { display: none; }
.zjzx-pane.active { display: block; }
.zjzx-course-toolbar {
display: flex; gap: 4px; margin-bottom: 4px; flex-wrap: wrap;
align-items: center;
}
.zjzx-course-toolbar button {
padding: 2px 10px; border-radius: 20px;
border: 2px solid #fde68a; background: #fffbeb;
font-size: 11px; cursor: pointer; font-weight: 700;
color: #92400e; transition: all 0.2s;
}
.zjzx-course-toolbar button:hover {
background: #fef3c7; border-color: #f59e0b; transform: scale(1.02);
}
.zjzx-course-toolbar button.zjzx-btn-unfinished {
background: #fef3c7; border-color: #f59e0b; color: #b45309;
}
.zjzx-course-toolbar .zjzx-toolbar-hint {
font-size: 10px; color: #b45309; margin-left: auto; font-weight: 600;
}
.zjzx-course-list {
max-height: 120px; overflow-y: auto;
border: 2px solid #fde68a; border-radius: 10px;
padding: 3px 6px; background: #fffbeb; margin-bottom: 4px;
}
.zjzx-course-item {
display: flex; align-items: center; gap: 6px;
padding: 3px 4px; border-bottom: 1px solid #fef3c7;
font-size: 12px; cursor: pointer; border-radius: 6px;
transition: background 0.15s;
}
.zjzx-course-item:last-child { border-bottom: none; }
.zjzx-course-item:hover { background: #fef3c7; }
.zjzx-course-item input[type="checkbox"] {
flex-shrink: 0; cursor: pointer; accent-color: #f59e0b;
width: 15px; height: 15px;
}
.zjzx-course-name {
flex: 1; overflow: hidden; text-overflow: ellipsis;
white-space: nowrap; font-weight: 500; color: #1e293b;
font-size: 12px;
}
.zjzx-course-progress {
font-size: 11px; color: #92400e; flex-shrink: 0;
margin-left: 4px; font-weight: 700;
}
.zjzx-course-status {
font-size: 10px; font-weight: 700; padding: 1px 8px;
border-radius: 20px; flex-shrink: 0;
border: 1px solid #fde68a;
}
.zjzx-course-status.learning { background: #fef3c7; color: #92400e; border-color: #fcd34d; }
.zjzx-course-status.exam { background: #dbeafe; color: #1d4ed8; border-color: #93c5fd; }
.zjzx-course-status.done { background: #d1fae5; color: #065f46; border-color: #6ee7b7; }
.zjzx-refresh-btn {
font-size: 11px; padding: 2px 10px; border-radius: 20px;
border: 2px solid #fde68a; background: #fffbeb;
cursor: pointer; font-weight: 700; color: #92400e;
transition: all 0.2s;
}
.zjzx-refresh-btn:hover { background: #fef3c7; border-color: #f59e0b; }
.zjzx-empty-state {
padding: 10px 8px; text-align: center; color: #b45309;
font-size: 13px; line-height: 1.8;
}
.zjzx-chapter-course {
margin-bottom: 6px;
border: 2px solid #fde68a;
border-radius: 10px;
overflow: hidden;
}
.zjzx-chapter-title {
padding: 5px 10px;
background: #fffbeb;
font-weight: 700;
font-size: 12px;
color: #92400e;
border-bottom: 1px solid #fde68a;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
.zjzx-chapter-item {
display: flex;
justify-content: space-between;
padding: 3px 10px;
font-size: 12px;
color: #78350f;
border-bottom: 1px solid #fef3c7;
}
.zjzx-chapter-item:last-child { border-bottom: none; }
.zjzx-log-box {
background: #fffbeb; color: #78350f; padding: 6px 8px;
border-radius: 10px; font-size: 11px; max-height: 200px;
overflow-y: auto; font-family: monospace; margin-top: 4px;
white-space: pre-wrap; word-break: break-all;
border: 2px solid #fde68a;
}
.zjzx-log-row {
padding: 2px 0; border-bottom: 1px solid #fef3c7;
font-size: 11px; color: #78350f;
}
.zjzx-log-actions {
display: flex; gap: 6px; margin-bottom: 4px;
}
.zjzx-log-actions button {
font-size: 11px; padding: 2px 10px; border-radius: 12px;
border: 1px solid #fde68a; background: #fffbeb;
cursor: pointer; color: #92400e; font-weight: 700;
transition: all 0.2s;
}
.zjzx-log-actions button:hover { background: #fef3c7; }
.zjzx-realtime-area {
border-top: 2px solid #fde68a; padding-top: 3px;
margin-top: 3px; flex-shrink: 0;
background: #fffbeb; padding-left: 10px; padding-right: 10px;
padding-bottom: 4px;
}
.zjzx-realtime-header {
display: flex; justify-content: space-between;
align-items: center; margin-bottom: 2px;
}
.zjzx-realtime-header span {
font-size: 11px; font-weight: 700; color: #92400e;
}
.zjzx-realtime-header button {
font-size: 10px; padding: 1px 8px; border-radius: 12px;
border: 1px solid #fde68a; background: #fff;
cursor: pointer; color: #92400e; font-weight: 600;
}
.zjzx-realtime-header button:hover { background: #fef3c7; }
.zjzx-realtime-log-box {
background: #fffbeb; color: #78350f; padding: 4px 8px;
border-radius: 8px; font-size: 10px; max-height: 60px;
overflow-y: auto; font-family: monospace;
white-space: pre-wrap; word-break: break-all;
border: 1px solid #fde68a;
}
.zjzx-realtime-log-row {
padding: 1px 0; border-bottom: 1px solid #fef3c7;
font-size: 10px; color: #78350f;
}
.zjzx-realtime-log-row:last-child { border-bottom: none; }
.zjzx-current-card {
background: #fffbeb; border: 2px solid #fde68a;
border-radius: 10px; padding: 4px 10px; margin: 3px 0 3px;
}
.zjzx-current-row {
display: flex; justify-content: space-between;
font-size: 12px; padding: 1px 0;
}
.zjzx-current-row .zjzx-label {
color: #92400e; font-weight: 600;
}
.zjzx-current-row .zjzx-value {
font-weight: 700; color: #78350f; text-align: right;
max-width: 60%; overflow: hidden; text-overflow: ellipsis;
white-space: nowrap;
}
.zjzx-auth-pane { padding: 6px 4px; }
.zjzx-auth-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 8px;
border-bottom: 1px solid #fde68a;
font-size: 13px;
}
.zjzx-auth-item .label { color: #92400e; font-weight: 600; }
.zjzx-auth-item .value { font-weight: 700; color: #78350f; }
.zjzx-auth-tip {
text-align: center;
font-size: 14px;
font-weight: bold;
color: #92400e;
padding: 4px 0;
}
.zjzx-auth-activate {
display: flex;
gap: 4px;
margin-top: 6px;
flex-wrap: wrap;
}
.zjzx-auth-activate input {
flex: 1;
padding: 4px 6px;
border-radius: 6px;
border: 1px solid #fde68a;
font-size: 12px;
min-width: 80px;
}
.zjzx-auth-activate button {
padding: 4px 12px;
border-radius: 20px;
border: 2px solid #f59e0b;
background: #fef3c7;
font-size: 12px;
font-weight: 700;
color: #92400e;
cursor: pointer;
}
.zjzx-auth-activate button:hover { background: #fde68a; }
.zjzx-footer-buy {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
background: #fffbeb;
border-top: 2px solid #fde68a;
flex-shrink: 0;
gap: 6px;
flex-wrap: wrap;
}
.zjzx-footer-buy .buy-label {
font-size: 12px; font-weight: 700; color: #92400e;
}
.zjzx-footer-buy .buy-buttons {
display: flex;
gap: 6px;
}
.zjzx-footer-buy .buy-btn {
padding: 2px 10px;
border-radius: 16px;
border: 1px solid #f59e0b;
background: #fef3c7;
color: #92400e;
font-weight: 700;
font-size: 12px;
cursor: pointer;
transition: background 0.2s;
}
.zjzx-footer-buy .buy-btn:hover {
background: #fde68a;
}
.zjzx-dot-green {
display: inline-block; width: 10px; height: 10px;
border-radius: 50%; background: #22c55e;
margin-right: 4px; box-shadow: 0 0 8px rgba(34,197,94,0.4);
}
.zjzx-dot-gray {
display: inline-block; width: 10px; height: 10px;
border-radius: 50%; background: #94a3b8;
margin-right: 4px;
}
#free-zjzx-panel ::-webkit-scrollbar { width: 4px; }
#free-zjzx-panel ::-webkit-scrollbar-track { background: transparent; }
#free-zjzx-panel ::-webkit-scrollbar-thumb { background: #fcd34d; border-radius: 4px; }
#free-zjzx-panel ::-webkit-scrollbar-thumb:hover { background: #f59e0b; }
`;
document.head.appendChild(style);
const panel = document.createElement('div');
panel.id = 'free-zjzx-panel';
panel.innerHTML = `
✖
⏳未学完 📝待考试 ✅已完成
📚 当前课程
无
⚙️ 当前模式
⚡ 效率模式
📌 当前任务
👉 点开始后自动提交
`;
document.body.appendChild(panel);
// ---- 拖动逻辑 ----
const header = document.getElementById('zjzx-panel-header');
let isDragging = false;
let startX, startY, startLeft, startTop;
try {
const pos = JSON.parse(localStorage.getItem(STORAGE_KEY_PANEL_POS));
if (pos && pos.left !== undefined && pos.top !== undefined) {
panel.style.left = pos.left + 'px';
panel.style.top = pos.top + 'px';
panel.style.right = 'auto';
}
} catch (_) {}
header.addEventListener('mousedown', (e) => {
if (e.target.closest('button')) return;
isDragging = true;
const rect = panel.getBoundingClientRect();
startX = e.clientX;
startY = e.clientY;
startLeft = rect.left;
startTop = rect.top;
panel.style.transition = 'none';
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
e.preventDefault();
});
function onMouseMove(e) {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
let newLeft = startLeft + dx;
let newTop = startTop + dy;
const maxX = window.innerWidth - panel.offsetWidth;
const maxY = window.innerHeight - panel.offsetHeight;
newLeft = Math.max(0, Math.min(newLeft, maxX));
newTop = Math.max(0, Math.min(newTop, maxY));
panel.style.left = newLeft + 'px';
panel.style.top = newTop + 'px';
panel.style.right = 'auto';
}
function onMouseUp() {
if (isDragging) {
isDragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
const rect = panel.getBoundingClientRect();
localStorage.setItem(STORAGE_KEY_PANEL_POS, JSON.stringify({ left: rect.left, top: rect.top }));
panel.style.transition = '';
}
}
document.querySelectorAll('.zjzx-tab-btn').forEach(el => {
el.addEventListener('click', function() {
switchTab(this.dataset.tab);
});
});
document.getElementById('zjzx-select-all').addEventListener('click', function() {
const allIds = state.courses.map(c => c.csId);
state.selected = allIds.slice();
renderCourseList();
updateUI();
log(`✅ 已全选 ${allIds.length} 门课程`);
});
document.getElementById('zjzx-deselect-all').addEventListener('click', function() {
state.selected = [];
renderCourseList();
updateUI();
log('❌ 已取消全选');
});
document.getElementById('zjzx-select-unfinished').addEventListener('click', function() {
const unfinishedIds = state.courses.filter(c => isUnfinished(c)).map(c => c.csId);
if (!unfinishedIds.length) {
log('🎯 所有课程已完成,无需勾选');
return;
}
state.selected = unfinishedIds.slice();
renderCourseList();
updateUI();
log(`🎯 已勾选 ${unfinishedIds.length} 门未完成课程(未学完 + 待考试)`);
});
document.getElementById('zjzx-refresh-courses').addEventListener('click', function() {
autoSelectUnfinished();
});
document.getElementById('zjzx-refresh-courses-extra').addEventListener('click', function() {
autoSelectUnfinished();
});
document.getElementById('zjzx-mode-select').addEventListener('change', function() {
state.mode = this.value;
document.getElementById('zjzx-current-chapter').textContent = getModeLabel(state.mode);
log(`切换为 ${getModeLabel(state.mode)}`);
});
document.getElementById('zjzx-multiplier-input').addEventListener('change', function() {
let val = parseFloat(this.value);
if (isNaN(val) || val < 1) val = 1;
if (val > 10) val = 10;
state.multiplier = val;
this.value = val;
log(`✖ 倍率设置为 ${val}`);
});
const examSlider = document.getElementById('zjzx-exam-slider');
const examDisplay = document.getElementById('zjzx-exam-display');
examSlider.addEventListener('input', function() {
const val = parseInt(this.value, 10);
examDisplay.textContent = val + 's';
state.examDuration = val;
localStorage.setItem('zjzx_exam_duration', String(val));
log(`📝 考试时长设置为 ${val} 秒`);
});
document.getElementById('zjzx-auto-sync').addEventListener('change', function() {
localStorage.setItem('zjzx_auto_sync', this.checked ? '1' : '0');
log(`🔄 自动同步 ${this.checked ? '已开启' : '已关闭'}`);
});
document.getElementById('zjzx-activate-btn').addEventListener('click', async function() {
const keyInput = document.getElementById('zjzx-license-key');
const key = keyInput.value.trim();
if (!key) { alert('请输入密钥'); return; }
const btn = this;
btn.textContent = '⏳ 验证中...';
btn.disabled = true;
try {
const result = await activateKey(key);
if (result.success) {
alert('✅ ' + result.message);
keyInput.value = '';
await checkAuth();
updateUI();
log('🔑 授权已更新:剩余 ' + state.cloudRemaining + ' 次');
if (state.modalVisible) {
closePurchaseModal();
}
} else {
alert('❌ ' + result.message);
}
} catch (e) {
alert('❌ 激活失败:' + e.message);
} finally {
btn.textContent = '激活';
btn.disabled = false;
}
});
document.getElementById('zjzx-buy-key-btn').addEventListener('click', function() {
const text = AUTHOR_QQ;
if (typeof GM_setClipboard === 'function') {
GM_setClipboard(text, 'text');
} else if (navigator.clipboard) {
navigator.clipboard.writeText(text);
} else {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
ta.remove();
}
alert('已复制作者QQ号:' + AUTHOR_QQ);
window.open(QQ_GROUP_LINK, '_blank');
});
document.getElementById('zjzx-footer-qq-link').addEventListener('click', function() {
try {
if (typeof GM_openInTab === 'function') {
GM_openInTab(QQ_GROUP_LINK, { active: true, insert: true, setParent: true });
} else {
window.open(QQ_GROUP_LINK, '_blank');
}
} catch (_) { window.open(QQ_GROUP_LINK, '_blank'); }
});
document.getElementById('zjzx-footer-copy-qq').addEventListener('click', function() {
const text = AUTHOR_QQ;
if (typeof GM_setClipboard === 'function') {
GM_setClipboard(text, 'text');
} else if (navigator.clipboard) {
navigator.clipboard.writeText(text);
} else {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
ta.remove();
}
alert('已复制作者QQ号:' + AUTHOR_QQ);
});
document.getElementById('zjzx-start-btn').addEventListener('click', startStudy);
document.getElementById('zjzx-stop-btn').addEventListener('click', stopStudy);
document.getElementById('zjzx-panel-toggle').addEventListener('click', function() {
const body = document.querySelector('#free-zjzx-panel .zjzx-panel-body');
const statusCard = document.querySelector('#free-zjzx-panel .zjzx-status-card');
const controls = document.querySelector('#free-zjzx-panel .zjzx-controls');
const settings = document.querySelector('#free-zjzx-panel .zjzx-settings-row');
const tabbar = document.querySelector('#free-zjzx-panel .zjzx-tabbar');
const realtime = document.querySelector('#free-zjzx-panel .zjzx-realtime-area');
const footer = document.querySelector('#free-zjzx-panel .zjzx-footer-buy');
if (body.style.display === 'none') {
body.style.display = '';
statusCard.style.display = '';
controls.style.display = '';
settings.style.display = '';
tabbar.style.display = '';
realtime.style.display = '';
footer.style.display = '';
this.textContent = '−';
} else {
body.style.display = 'none';
statusCard.style.display = 'none';
controls.style.display = 'none';
settings.style.display = 'none';
tabbar.style.display = 'none';
realtime.style.display = 'none';
footer.style.display = 'none';
this.textContent = '+';
}
});
document.getElementById('zjzx-copy-log').addEventListener('click', function() {
const text = state.logLines.join('\n');
navigator.clipboard.writeText(text).then(() => alert('📋 日志已复制')).catch(() => {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
ta.remove();
alert('📋 日志已复制');
});
});
document.getElementById('zjzx-clear-log').addEventListener('click', function() {
state.logLines = [];
displayedSet = new Set();
updateLogUI();
log('🗑️ 完整日志已清空');
});
document.getElementById('zjzx-clear-realtime').addEventListener('click', function() {
const realtimeBox = document.getElementById('zjzx-realtime-log');
if (realtimeBox) {
realtimeBox.innerHTML = '⏳ 已清空
';
}
});
refreshCourseListData(true).then(() => autoSelectUnfinished());
(async function initAuth() {
await checkAuth();
updateUI();
if (state.cloudAuthorized && state.cloudRemaining > 0) {
log(`📊 剩余免费次数:${state.cloudRemaining}`);
} else {
const freeUsed = localStorage.getItem(FREE_TRIAL_KEY) === '1';
if (!freeUsed) {
log('🔒 免费次数已用完,请购买密钥激活 (作者QQ:' + AUTHOR_QQ + ')');
} else {
log('🔒 免费次数已用完,请购买密钥激活 (作者QQ:' + AUTHOR_QQ + ')');
}
setTimeout(() => {
const freeUsedNow = localStorage.getItem(FREE_TRIAL_KEY) === '1';
if (!state.modalVisible && state.cloudRemaining === 0 && freeUsedNow) {
showPurchaseModal();
}
}, 3000);
}
})();
updateUI();
updateLogUI();
log('🌈 安徽专业技术继续教育公需一学习助手 v1.3 已加载');
log('📊 使用记录已开启(匿名统计使用情况,帮助改进脚本)');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createUI);
} else {
createUI();
}
})();