// ==UserScript==
// @name 【效率版】河南专技培训平台学习助手(河南继续教育学会/南阳理工学院)
// @namespace https://card.wlxy.live/details/C7467074
// @version 1.1
// @description 河南继续教育学会(jxjyedu.org.cn)、南阳理工学院(nypx.jxjyedu.org.cn)专业技术人员继续教育助手:已报名公需课、专业课效率快速完成;免费每站3章节,Pro本站激活绑定不限。
// @antifeature payment 免费体验3个视频章节,升级Pro不限
// @antifeature membership 需云端授权
// @author 柠檬真酸
// @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png
// @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
// @connect www.jxjyedu.org.cn
// @connect jxjyedu.org.cn
// @connect nypx.jxjyedu.org.cn
// @connect oa25.ahzsksw.cn
// @connect card.wlxy.live
// @connect huaweicloudobs.ahjxjy.cn
// @connect cdn2.sxzjpx.cn
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_openInTab
// @grant unsafeWindow
// @run-at document-idle
// @tag 河南继续教育
// @tag 南阳理工学院
// @tag 专技继续教育
// @tag 河南专技
// @license All Rights Reserved
// ==/UserScript==
(function () {
"use strict";
const SCRIPT_VERSION = "1.3.6";
try {
if (window.top !== window.self) return;
} catch (_) {
return;
}
const SITE_HOST = String(location.hostname || "")
.replace(/^www\./i, "")
.toLowerCase();
const JXJY_SUPPORTED_HOSTS = new Set(["jxjyedu.org.cn", "nypx.jxjyedu.org.cn"]);
const LYLG_SUPPORTED_HOSTS = new Set([
"ly.fnhzj.com",
"huayuzj.com",
"testplugservice-v3.ylxue.net",
"plugservice-v3.ylxue.net",
]);
const REDIRECT_ONLY_HOSTS = new Set([
"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",
]);
const HN_REDIRECT_BOOT_KEY = "__HN_ZJ_PORTAL_REDIRECT_V1__";
const HN_SCRIPT_JXJY_URL = "https://scriptcat.org/zh-CN/script-show-page/7693";
const HN_SCRIPT_LYLG_URL = "https://scriptcat.org/zh-CN/script-show-page/7691";
const HN_ADAPTED_PLATFORMS = [
{
name: "\u6cb3\u5357\u7701\u7ee7\u7eed\u6559\u80b2\u5b66\u4f1a",
url: "https://www.jxjyedu.org.cn/",
modeLabel: "\u6548\u7387\u5feb\u901f\u5b8c\u6210",
modeClass: "hn-zj-badge-fast",
scriptUrl: HN_SCRIPT_JXJY_URL,
scriptLabel: "\u5b89\u88c5\u5b66\u4e60\u52a9\u624b\uff08ScriptCat #7693\uff09",
},
{
name: "\u5357\u9633\u7406\u5de5\u5b66\u9662",
url: "https://nypx.jxjyedu.org.cn/",
modeLabel: "\u6548\u7387\u5feb\u901f\u5b8c\u6210",
modeClass: "hn-zj-badge-fast",
scriptUrl: HN_SCRIPT_JXJY_URL,
scriptLabel: "\u5b89\u88c5\u5b66\u4e60\u52a9\u624b\uff08ScriptCat #7693\uff09",
},
{
name: "\u6d1b\u9633\u7406\u5de5\u5b66\u9662",
url: "https://www.ly.fnhzj.com/",
modeLabel: "1:1 \u591a\u8bfe\u540c\u5237",
modeClass: "hn-zj-badge-multi",
scriptUrl: HN_SCRIPT_LYLG_URL,
scriptLabel: "\u5b89\u88c5\u5b66\u4e60\u52a9\u624b\uff08ScriptCat #7691\uff09",
},
{
name: "\u4e2d\u539f\u5de5\u5b66\u9662",
url: "https://www.huayuzj.com/",
modeLabel: "1:1 \u591a\u8bfe\u540c\u5237",
modeClass: "hn-zj-badge-multi",
scriptUrl: HN_SCRIPT_LYLG_URL,
scriptLabel: "\u5b89\u88c5\u5b66\u4e60\u52a9\u624b\uff08ScriptCat #7691\uff09",
},
];
const REDIRECT_SOURCE_LABELS = {
"hnzjgl.gov.cn": "\u6cb3\u5357\u4e13\u6280\u7ba1\u7406\u5e73\u53f0",
"jxjy.henu.edu.cn": "\u6cb3\u5357\u5927\u5b66",
"hnpihn.newzhihui.cn": "\u6cb3\u5357\u5de5\u4e1a\u804c\u4e1a\u6280\u672f\u5927\u5b66",
"zdkj.v.zzu.edu.cn": "\u90d1\u5dde\u5927\u5b66",
"hnzj.ghlearning.com": "\u6cb3\u5357\u9ad8\u8f89\u6559\u80b2\u79d1\u6280\u6709\u9650\u516c\u53f8",
"hnzj.user.ghlearning.com": "\u6cb3\u5357\u9ad8\u8f89\u6559\u80b2\u79d1\u6280\u6709\u9650\u516c\u53f8",
"zyjs.lypt.edu.cn": "\u6d1b\u9633\u804c\u4e1a\u6280\u672f\u5b66\u9662",
};
function resolveHostRole(host) {
if (JXJY_SUPPORTED_HOSTS.has(host)) return "jxjy";
if (LYLG_SUPPORTED_HOSTS.has(host)) return "lylg";
if (REDIRECT_ONLY_HOSTS.has(host)) return "redirect";
return "redirect";
}
function injectHnRedirectStyles() {
if (document.getElementById("hn-zj-redirect-style")) return;
const st = document.createElement("style");
st.id = "hn-zj-redirect-style";
st.textContent = `
#hn-zj-redirect-panel{position:fixed;right:20px;top:80px;z-index:999999;width:390px;background:#f4f7fb;border:1px solid #dbe4f0;border-radius:16px;box-shadow:0 16px 40px rgba(15,23,42,.17);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;font-size:12px;color:#0f172a;overflow:hidden;}
#hn-zj-redirect-header{padding:10px 12px;background:linear-gradient(180deg,#f8ecd5,#f4e8cf);border-bottom:1px solid #e5dbc6;cursor:move;user-select:none;display:flex;align-items:center;gap:9px;}
#hn-zj-redirect-logo{width:28px;height:28px;border-radius:8px;object-fit:cover;background:#fff;border:1px solid rgba(148,163,184,.45);}
#hn-zj-redirect-title{font-size:13px;font-weight:900;color:#9a3412;line-height:1.3;flex:1;}
#hn-zj-redirect-body{padding:10px 12px 12px;}
.hn-zj-redirect-lead{font-size:12px;color:#475569;line-height:1.55;margin:0 0 8px;}
.hn-zj-redirect-lead strong{color:#9a3412;}
.hn-zj-redirect-list{display:flex;flex-direction:column;gap:7px;margin:0;padding:0;list-style:none;}
.hn-zj-redirect-item{background:#fff;border:1px solid #dbe4f0;border-radius:11px;padding:8px 9px;}
.hn-zj-redirect-item-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px;}
.hn-zj-redirect-link{font-size:13px;font-weight:800;color:#1d4ed8;text-decoration:none;}
.hn-zj-redirect-link:hover{text-decoration:underline;}
.hn-zj-redirect-item-foot{margin-top:5px;}
.hn-zj-redirect-script{font-size:11px;font-weight:700;color:#7c2d12;text-decoration:none;}
.hn-zj-redirect-script:hover{text-decoration:underline;color:#9a3412;}
.hn-zj-badge{font-size:10px;font-weight:800;padding:2px 7px;border-radius:999px;white-space:nowrap;flex:0 0 auto;}
.hn-zj-badge-fast{color:#0f766e;background:#ecfdf5;border:1px solid #99f6e4;}
.hn-zj-badge-multi{color:#1d4ed8;background:#eff6ff;border:1px solid #bfdbfe;}
.hn-zj-redirect-hint{margin:9px 0 0;font-size:11px;color:#64748b;line-height:1.45;background:linear-gradient(180deg,#fffdf5,#fff7e6);border:1px solid #fcd34d;border-radius:10px;padding:7px 9px;}
`;
document.head.appendChild(st);
}
function bindHnRedirectDrag(panel) {
const header = panel.querySelector("#hn-zj-redirect-header");
if (!header) return;
let sx = 0,
sy = 0,
ox = 0,
oy = 0,
dragging = false;
header.addEventListener("mousedown", (e) => {
dragging = true;
sx = e.clientX;
sy = e.clientY;
const rect = panel.getBoundingClientRect();
ox = rect.left;
oy = rect.top;
e.preventDefault();
});
window.addEventListener("mousemove", (e) => {
if (!dragging) return;
panel.style.left = `${ox + e.clientX - sx}px`;
panel.style.top = `${oy + e.clientY - sy}px`;
panel.style.right = "auto";
});
window.addEventListener("mouseup", () => {
dragging = false;
});
}
function _hnEsc(s) {
return String(s || "")
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
}
function renderHnRedirectPlatformItems() {
return HN_ADAPTED_PLATFORMS.map(
(p) =>
`
`
).join("");
}
function runHnRedirectPanelOnly() {
try {
const root =
(typeof unsafeWindow !== "undefined" && unsafeWindow.top) ||
window.top ||
window;
if (root[HN_REDIRECT_BOOT_KEY]) return;
root[HN_REDIRECT_BOOT_KEY] = SCRIPT_VERSION;
} catch (_) {
if (window[HN_REDIRECT_BOOT_KEY]) return;
window[HN_REDIRECT_BOOT_KEY] = SCRIPT_VERSION;
}
injectHnRedirectStyles();
document.querySelectorAll("#hn-zj-redirect-panel").forEach((el) => el.remove());
const sourceLabel = REDIRECT_SOURCE_LABELS[SITE_HOST] || "\u5f53\u524d\u5e73\u53f0";
const panel = document.createElement("div");
panel.id = "hn-zj-redirect-panel";
panel.innerHTML = `
\u60a8\u5f53\u524d\u8bbf\u95ee\u7684\u662f ${_hnEsc(sourceLabel)}\uff0c\u6682\u672a\u63a5\u5165\u81ea\u52a8\u5b66\u4e60\u3002
\u5efa\u8bae\u4f18\u5148\u524d\u5f80\u4ee5\u4e0b 4 \u4e2a\u5df2\u9002\u914d\u5e73\u53f0 \u5b66\u4e60\uff1a
${renderHnRedirectPlatformItems()}
\u524d\u5f80\u5e73\u53f0\u524d\u8bf7\u5148\u5b89\u88c5\u5bf9\u5e94 ScriptCat \u811a\u672c\u3002\u6cb3\u5357\u7701\u7ee7\u7eed\u6559\u80b2\u5b66\u4f1a\u3001\u5357\u9633\u7406\u5de5\u5b66\u9662\u652f\u6301\u6548\u7387\u5feb\u901f\u5b8c\u6210\uff08#7693\uff09\uff1b\u6d1b\u9633\u7406\u5de5\u5b66\u9662\u3001\u4e2d\u539f\u5de5\u5b66\u9662\u652f\u6301 1:1 \u65f6\u957f\u591a\u8bfe\u540c\u65f6\u5b66\u4e60\uff08#7691\uff09\u3002
`;
document.body.appendChild(panel);
bindHnRedirectDrag(panel);
}
const HOST_ROLE = resolveHostRole(SITE_HOST);
if (HOST_ROLE === "lylg") return;
if (HOST_ROLE === "redirect") {
runHnRedirectPanelOnly();
return;
}
const BOOT_KEY = "__JXJY_PROGRESS_HELPER_BOOTED__";
try {
const root =
(typeof unsafeWindow !== "undefined" && unsafeWindow.top) ||
window.top ||
window;
if (root[BOOT_KEY]) return;
root[BOOT_KEY] = SCRIPT_VERSION;
} catch (_) {
if (window[BOOT_KEY]) return;
window[BOOT_KEY] = SCRIPT_VERSION;
}
const SITE_HOST_NORM = SITE_HOST;
const SITE_REGISTRY = {
"jxjyedu.org.cn": {
id: "jxjy",
name: "\u6cb3\u5357\u7ee7\u7eed\u6559\u80b2\u5b66\u4f1a\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2\u52a9\u624b",
webSite: "www.jxjyedu.org.cn",
},
"nypx.jxjyedu.org.cn": {
id: "nypx",
name: "\u5357\u9633\u7406\u5de5\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2\u52a9\u624b",
webSite: "nypx.jxjyedu.org.cn",
},
};
const SITE_PREF_KEY = "jxjyedu_active_site_v1";
function resolveActiveSiteHost() {
if (SITE_REGISTRY[SITE_HOST_NORM]) return SITE_HOST_NORM;
try {
const pref = GM_getValue(SITE_PREF_KEY);
if (pref && SITE_REGISTRY[pref]) return pref;
} catch (_) {}
return "jxjyedu.org.cn";
}
const ACTIVE_SITE_HOST = resolveActiveSiteHost();
const ACTIVE_SITE = SITE_REGISTRY[ACTIVE_SITE_HOST] || SITE_REGISTRY["jxjyedu.org.cn"];
const SITE_NAME = ACTIVE_SITE.name;
const SITE_ID = ACTIVE_SITE.id;
const WEB_SITE = ACTIVE_SITE.webSite;
const CLOUD_API_PREFIX = `/api/${SITE_ID}/`;
try {
if (SITE_REGISTRY[SITE_HOST_NORM]) GM_setValue(SITE_PREF_KEY, SITE_HOST_NORM);
} catch (_) {}
const PANEL_LOGO_URL =
"https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png";
const PANEL_FEATURE_SLOGAN = "\u8fdb\u5ea6\u540c\u6b65";
const CLOUD_API_BASE = "https://oa25.ahzsksw.cn";
const PRO_BUY_URL = "https://card.wlxy.live/details/C7467074";
const CLOUD_TOKEN_KEY = `${SITE_ID}_cloud_token_v1`;
const CLOUD_LEASE_KEY = `${SITE_ID}_cloud_lease_v1`;
const PANEL_POS_KEY = `${SITE_ID}_panel_pos_v1`;
const PANEL_COLLAPSED_KEY = `${SITE_ID}_panel_collapsed_v1`;
const FINISH_TIP =
"\u6240\u9009\u8bfe\u7a0b\u5df2\u5168\u90e8\u5b66\u5b8c\u3002\u82e5\u5e73\u53f0\u8fdb\u5ea6\u7a0d\u540e\u66f4\u65b0\u5c5e\u6b63\u5e38\u73b0\u8c61\uff0c\u53ef\u5237\u65b0\u9875\u9762\u67e5\u770b\u3002";
const PANEL_NOTICE =
`\u514d\u8d39\u4f53\u9a8c 3 \u4e2a\u89c6\u9891\u7ae0\u8282\uff08${SITE_NAME} \u72ec\u7acb\u8ba1\u6570\uff09\uff1bPro \u672c\u7ad9\u6fc0\u6d3b\u540e\u7ed1\u5b9a\u672c\u7ad9\u3002\u8d2d\u4e70\uff1a` +
PRO_BUY_URL;
const QQ_GROUP_URL = "";
const QQ_GROUP_LABEL = "\u4ea4\u6d41\u7fa4";
const LOG_PALETTE = [
"#0369a1",
"#0f766e",
"#b45309",
"#a21caf",
"#be123c",
"#1d4ed8",
"#047857",
"#c2410c",
"#7c3aed",
"#0e7490",
"#4f46e5",
"#ca8a04",
];
const LOG_LIMIT = 400;
const COMPLETE_THRESHOLD = 95;
const REPORT_INTERVAL = 30;
const TURBO_MODE = true;
const TURBO_STEP_SEC = 300;
const TURBO_DELAY_MS = 600;
const API_BASE = "/wx/mp";
const state = {
student: null,
years: [],
selectedYear: null,
courses: [],
selectedCourseKeys: new Set(),
chapterPreview: [],
logLines: [],
loading: false,
running: false,
stopFlag: false,
currentTask: "",
cloudToken: "",
cloudLease: "",
cloudLeaseExp: 0,
cloudTier: "free",
cloudProExpireAt: 0,
tokenBoundBlocked: false,
freeChapterLimit: 3,
freeUsedChapters: 0,
proBuyUrl: PRO_BUY_URL,
finishTip: FINISH_TIP,
panelNotice: PANEL_NOTICE,
};
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 syncSelectedCourseKeys() {
const year = state.selectedYear;
if (!year) {
state.selectedCourseKeys = new Set();
return;
}
const valid = new Set(
state.courses
.filter((c) => Number(c.year) === Number(year))
.map((c) => courseKey(c.year, c.id))
);
state.selectedCourseKeys = new Set(
[...state.selectedCourseKeys].filter((k) => valid.has(k))
);
if (state.selectedCourseKeys.size) return;
const incomplete = state.courses
.filter(
(c) =>
Number(c.year) === Number(year) &&
(Number(c.progress) || 0) < COMPLETE_THRESHOLD
)
.map((c) => courseKey(c.year, c.id));
if (incomplete.length) {
state.selectedCourseKeys = new Set(incomplete);
}
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
function openUrl(url) {
const u = String(url || "").trim();
if (!u) return;
try {
if (typeof GM_openInTab === "function") GM_openInTab(u, { active: true });
else window.open(u, "_blank");
} catch (_) {
try {
window.open(u, "_blank");
} catch (e) {}
}
}
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 formatTime(sec) {
sec = Math.max(0, Math.floor(Number(sec) || 0));
const h = Math.floor(sec / 3600);
const m = Math.floor((sec % 3600) / 60);
const s = sec % 60;
if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
}
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 courseListProgress(c) {
const cc = Number(c.chapter_count) || 0;
const done = Number(c.completed_count) || 0;
if (cc > 0) return Math.round((done / cc) * 100);
return Math.min(100, Math.max(0, Number(c.progress) || 0));
}
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 log(_msg) {}
function logColorForKey(key) {
const s = String(key || "");
let h = 2166136261;
for (let i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i);
h = Math.imul(h, 16777619);
}
return LOG_PALETTE[h % LOG_PALETTE.length];
}
function inferLogKind(msg) {
if (/\u5b8c\u6210$|\u5904\u7406\u5b8c\u6bd5|\u5168\u90e8/.test(msg)) return "done";
if (/\u6ca1\u6709|\u8bf7\u5148|\u4e2d\u65ad|\u5931\u8d25|\u9519\u8bef|\u672a\u5b8c\u6210|\u767b\u5f55/.test(msg)) return "info";
return "progress";
}
function renderLogRow(entry) {
if (typeof entry === "string") {
return `${escHtml(entry)}
`;
}
const kind = entry.kind || "progress";
const color =
kind === "info" ? "#64748b" : logColorForKey(entry.key || entry.msg);
let body = escHtml(entry.msg);
if (kind === "progress") {
body = body
.replace(/(\u5df2\u5b66[\d.]+%)/g, '$1')
.replace(/(\d+\u5206\u949f\/\u603b\d+\u5206)/g, '$1');
} else if (kind === "done") {
body = body.replace(/(\u5b8c\u6210)/g, '$1');
}
return `
[${escHtml(entry.time)}] ${body}
`;
}
function _plog(msg, colorKey) {
const entry = {
time: new Date().toLocaleTimeString(),
msg: String(msg),
key: colorKey || String(msg).slice(0, 24),
kind: inferLogKind(msg),
};
state.logLines.unshift(entry);
if (state.logLines.length > LOG_LIMIT) state.logLines.length = LOG_LIMIT;
const box = document.getElementById("jxjy-run-log");
if (box) {
box.innerHTML = state.logLines.map(renderLogRow).join("");
box.scrollTop = 0;
}
}
function setCurrentTask(text) {
state.currentTask = String(text || "");
const el = document.getElementById("jxjy-current-task");
if (el) el.textContent = state.currentTask || "\u5f85\u547d";
}
function syncRunButtons() {
const start = document.getElementById("jxjy-start");
const stop = document.getElementById("jxjy-stop");
if (start) {
start.disabled = state.running;
start.classList.toggle("jxjy-btn-off", state.running);
}
if (stop) {
stop.disabled = !state.running;
stop.classList.toggle("jxjy-btn-off", !state.running);
}
}
function showLogTab() {
const panel = document.getElementById("jxjy-auto-panel");
if (!panel) return;
panel.querySelectorAll(".jxjy-tab-btn").forEach((b) => b.classList.remove("active"));
panel.querySelectorAll(".jxjy-pane").forEach((p) => p.classList.remove("active"));
panel.querySelector('.jxjy-tab-btn[data-tab="log"]')?.classList.add("active");
panel.querySelector('.jxjy-pane[data-pane="log"]')?.classList.add("active");
}
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("\u54cd\u5e94\u975e JSON");
}
if (Number(data.code) === -1) {
throw new Error("\u767b\u5f55\u5df2\u8fc7\u671f\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55");
}
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("\u54cd\u5e94\u975e JSON");
}
if (Number(data.code) === -1) {
throw new Error("\u767b\u5f55\u5df2\u8fc7\u671f\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55");
}
return data;
}
async function fetchStudentInfo() {
const data = await apiGet("/StudentInfo");
if (Number(data.code) !== 0 || !data.student) {
throw new Error(data.msg || "\u672a\u767b\u5f55\uff0c\u8bf7\u5148\u767b\u5f55\u5e73\u53f0");
}
state.student = data.student;
updateAuthUi();
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 || "\u52a0\u8f7d\u8bfe\u7a0b\u5931\u8d25");
}
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 || "\u52a0\u8f7d\u7ae0\u8282\u5931\u8d25");
}
return data;
}
function mapCourseRow(row, year) {
const progress = courseListProgress(row);
return {
id: Number(row.id) || 0,
name: String(row.name || "\u672a\u547d\u540d\u8bfe\u7a0b"),
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 || "\u7ae0\u8282"),
duration,
position,
progress,
url: ch.url,
courseid: Number(ch.courseid || courseId) || courseId,
done,
status: done ? "completed" : progress > 0 ? "learning" : "notstarted",
};
}
async function reportCourseStudy(params) {
const data = await apiPostForm("/CourseStudy", params);
if (Number(data.code) > 0) {
throw new Error(data.msg || "\u4e0a\u62a5\u5931\u8d25");
}
return data;
}
function getCloudToken() {
try {
const t = String(GM_getValue(CLOUD_TOKEN_KEY, "") || state.cloudToken || "").trim();
state.cloudToken = t;
return t;
} catch (_) {
return String(state.cloudToken || "").trim();
}
}
function setCloudToken(token) {
const t = String(token || "").trim();
state.cloudToken = t;
try {
GM_setValue(CLOUD_TOKEN_KEY, t);
} catch (_) {}
}
function getLearningUserId() {
const s = state.student || {};
return String(s.id || s.userid || s.uuid || "").trim();
}
function _gj(url, opts = {}) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: opts.method || "GET",
url,
headers: opts.headers || {},
data: opts.data,
timeout: opts.timeout || 60000,
onload(res) {
resolve({
status: res.status,
text: String(res.responseText || ""),
});
},
onerror: () => reject(new Error("\u4e91\u7aef\u7f51\u7edc\u5931\u8d25")),
ontimeout: () => reject(new Error("\u4e91\u7aef\u8bf7\u6c42\u8d85\u65f6")),
});
});
}
function _pcd(data, status) {
let detail = data && (data.detail != null ? data.detail : data.message || data.code);
if (detail && typeof detail === "object") {
if (
detail.code === "free_quota_exceeded" ||
/free_quota/i.test(String(detail.code || "")) ||
/\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c/.test(String(detail.message || ""))
) {
if (detail.proBuyUrl) state.proBuyUrl = String(detail.proBuyUrl).trim() || PRO_BUY_URL;
if (detail.free_video_limit != null || detail.free_chapter_limit != null) {
state.freeChapterLimit =
Number(detail.free_chapter_limit ?? detail.free_video_limit) || 3;
}
if (detail.free_used_videos != null || detail.free_used_chapters != null) {
state.freeUsedChapters =
Number(detail.free_used_chapters ?? detail.free_used_videos) || 0;
}
const err = new Error("free_quota_exceeded");
err.code = "free_quota_exceeded";
err.detail = detail;
return err;
}
if (detail.code === "pro_required" || /pro_required/i.test(String(detail.code || ""))) {
if (detail.proBuyUrl) state.proBuyUrl = String(detail.proBuyUrl).trim() || PRO_BUY_URL;
const err = new Error("pro_required");
err.code = "pro_required";
err.detail = detail;
return err;
}
const code = String(detail.code || detail.message || "");
if (/token_bound/i.test(code) || /\u5df2\u7ed1\u5b9a/i.test(String(detail.message || ""))) {
const err = new Error(String(detail.message || "token_bound"));
err.code = String(detail.code || "token_bound_other_user");
err.detail = detail;
return err;
}
return new Error(String(detail.message || detail.code || JSON.stringify(detail)));
}
return new Error(String(detail || `\u4e91\u7aef HTTP ${status}`));
}
async function _crq(path, method, payload) {
const base = CLOUD_API_BASE.replace(/\/$/, "");
const url = base + (path.startsWith("/") ? path : "/" + path);
const headers = {
Accept: "application/json",
"Content-Type": "application/json",
};
const token = getCloudToken();
if (token && !state.tokenBoundBlocked) headers.Authorization = "Bearer " + token;
const luid = getLearningUserId();
if (luid) headers["x-learning-user-id"] = luid;
let bodyObj = payload;
if (bodyObj && typeof bodyObj === "object") {
bodyObj = Object.assign({}, bodyObj);
if (path !== `${CLOUD_API_PREFIX}lease` && state.cloudLease && bodyObj.lease == null) {
bodyObj.lease = state.cloudLease;
}
if (path === `${CLOUD_API_PREFIX}lease` && state.tokenBoundBlocked) bodyObj.token = "";
}
const res = await _gj(url, {
method: method || "POST",
headers,
data: bodyObj == null ? null : JSON.stringify(bodyObj),
});
let data = {};
const text = String(res.text || "").trim();
if (text) {
try {
data = JSON.parse(text);
} catch (_) {
data = { message: text.slice(0, 200) };
}
}
if (res.status < 200 || res.status >= 300) {
throw _pcd(data, res.status);
}
return data;
}
function _alp(data) {
if (!data || typeof data !== "object") return;
if (data.lease) {
state.cloudLease = String(data.lease || "");
state.cloudLeaseExp =
Number(data.lease_expires_at || data.exp || 0) ||
Math.floor(Date.now() / 1000) + 600;
try {
GM_setValue(CLOUD_LEASE_KEY, {
lease: state.cloudLease,
exp: state.cloudLeaseExp,
tier: String(data.tier || ""),
freeChapterLimit: Number(
data.free_chapter_limit ?? data.free_video_limit ?? data.freeChapterLimit ?? 3
),
freeUsedChapters: Number(
data.free_used_chapters ?? data.free_used_videos ?? data.freeUsedChapters ?? 0
),
proExpireAt: Number(data.pro_expires_at || data.proExpireAt || 0) || 0,
boundUid: String(data.bound_learning_user_id || getLearningUserId() || ""),
proBuyUrl: String(data.proBuyUrl || state.proBuyUrl || PRO_BUY_URL),
finishTip: String(data.finishTip || state.finishTip || FINISH_TIP),
panelNotice: String(data.panelNotice || state.panelNotice || PANEL_NOTICE),
});
} catch (_) {}
}
if (data.tier) state.cloudTier = String(data.tier);
if (data.free_chapter_limit != null || data.free_video_limit != null || data.freeChapterLimit != null) {
state.freeChapterLimit =
Number(data.free_chapter_limit ?? data.free_video_limit ?? data.freeChapterLimit) || 3;
}
if (data.free_used_chapters != null || data.free_used_videos != null || data.freeUsedChapters != null) {
state.freeUsedChapters =
Number(data.free_used_chapters ?? data.free_used_videos ?? data.freeUsedChapters) || 0;
}
if (data.pro_expires_at != null || data.proExpireAt != null) {
state.cloudProExpireAt = Number(data.pro_expires_at ?? data.proExpireAt) || 0;
}
if (data.proBuyUrl) state.proBuyUrl = String(data.proBuyUrl).trim() || PRO_BUY_URL;
if (data.finishTip) state.finishTip = String(data.finishTip);
if (data.panelNotice) state.panelNotice = String(data.panelNotice);
_ucp();
_upn();
}
function _lcl() {
try {
const cached = GM_getValue(CLOUD_LEASE_KEY, null);
if (!cached || typeof cached !== "object") return null;
const lease = String(cached.lease || "").trim();
const exp = Number(cached.exp || 0);
if (!lease || !exp) return null;
if (exp - Math.floor(Date.now() / 1000) <= 60) return null;
const boundUid = String(cached.boundUid || "").trim();
const uid = getLearningUserId();
if (uid && boundUid && uid !== boundUid) return null;
state.cloudLease = lease;
state.cloudLeaseExp = exp;
if (cached.tier) state.cloudTier = String(cached.tier);
if (cached.freeChapterLimit != null) state.freeChapterLimit = Number(cached.freeChapterLimit) || 3;
if (cached.freeUsedChapters != null) state.freeUsedChapters = Number(cached.freeUsedChapters) || 0;
if (cached.proExpireAt != null) state.cloudProExpireAt = Number(cached.proExpireAt) || 0;
if (cached.proBuyUrl) state.proBuyUrl = String(cached.proBuyUrl);
if (cached.finishTip) state.finishTip = String(cached.finishTip);
if (cached.panelNotice) state.panelNotice = String(cached.panelNotice);
return cached;
} catch (_) {
return null;
}
}
function _icc() {
state.cloudLease = "";
state.cloudLeaseExp = 0;
try {
GM_setValue(CLOUD_LEASE_KEY, null);
} catch (_) {}
}
async function _cl(force) {
const now = Math.floor(Date.now() / 1000);
if (!force && state.cloudLease && state.cloudLeaseExp - now > 60) return true;
if (!force && _lcl()) {
_ucp();
return true;
}
const postLease = async (tokenOverride) =>
_crq(`${CLOUD_API_PREFIX}lease`, "POST", {
learning_user_id: getLearningUserId(),
userid: getLearningUserId(),
site_id: SITE_ID,
token: tokenOverride != null ? tokenOverride : state.tokenBoundBlocked ? "" : getCloudToken(),
});
let data;
try {
data = await postLease();
state.tokenBoundBlocked = false;
} catch (e) {
if (String(e && e.code ? e.code : e.message || e).includes("token_bound")) {
state.tokenBoundBlocked = true;
_icc();
state.cloudTier = "free";
state.cloudProExpireAt = 0;
const tip =
(e && e.detail && e.detail.message) ||
e.message ||
"Pro Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u8d26\u53f7\u6216\u7ad9\u70b9";
_plog(String(tip));
data = await postLease("");
} else {
throw e;
}
}
_alp(data);
if (!state.cloudLeaseExp) state.cloudLeaseExp = now + 600;
return !!state.cloudLease;
}
async function _es(context) {
await _cl(false);
const data = await _crq(`${CLOUD_API_PREFIX}study/engine/start`, "POST", {
lease: state.cloudLease,
kind: "vod",
context: context || {},
config: {
report_interval_sec: REPORT_INTERVAL,
complete_threshold: COMPLETE_THRESHOLD,
},
});
if (data.free_used_chapters != null || data.free_used_videos != null) {
state.freeUsedChapters =
Number(data.free_used_chapters ?? data.free_used_videos) || 0;
}
if (data.free_chapter_limit != null || data.free_video_limit != null) {
state.freeChapterLimit =
Number(data.free_chapter_limit ?? data.free_video_limit) || 3;
}
if (data.tier) state.cloudTier = String(data.tier);
if (data.proBuyUrl) state.proBuyUrl = String(data.proBuyUrl).trim() || PRO_BUY_URL;
_ucp();
return data;
}
async function _estep(sessionId, event, lastResult) {
await _cl(false);
return _crq(`${CLOUD_API_PREFIX}study/engine/step`, "POST", {
lease: state.cloudLease,
session_id: String(sessionId || ""),
event: String(event || "chapter_done"),
last_result: lastResult == null ? null : lastResult,
});
}
function _ifqe(e) {
return String(e && e.code ? e.code : e.message || e).includes("free_quota");
}
function _ifqx() {
return state.cloudTier !== "pro" && state.freeUsedChapters >= state.freeChapterLimit;
}
async function _hfq() {
const url = String(state.proBuyUrl || PRO_BUY_URL).trim() || PRO_BUY_URL;
_plog(`\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff08${state.freeUsedChapters}/${state.freeChapterLimit}\uff09`);
_ucp();
const ok = await _spc("\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff0c\u662f\u5426\u6253\u5f00 Pro \u8d2d\u4e70\u9875\uff1f", {
title: "\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c",
confirmText: "\u6253\u5f00\u8d2d\u4e70\u9875",
cancelText: "\u7a0d\u540e\u518d\u8bf4",
});
if (ok) openUrl(url);
}
function formatProExpireAt(ts) {
const n = Number(ts) || 0;
if (!n) return "";
const ms = n > 1e12 ? n : n * 1000;
const d = new Date(ms);
if (Number.isNaN(d.getTime())) return "";
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 _ucp() {
const tierEl = document.getElementById("jxjy-cloud-tier");
const quotaEl = document.getElementById("jxjy-cloud-quota");
const expireEl = document.getElementById("jxjy-cloud-expire");
const buyBtn = document.getElementById("jxjy-buy-pro");
const settingsTier = document.getElementById("jxjy-settings-tier");
const settingsExpire = document.getElementById("jxjy-settings-expire");
const isPro = state.cloudTier === "pro";
const expireText = formatProExpireAt(state.cloudProExpireAt);
if (tierEl) {
tierEl.textContent = isPro ? "Pro" : "\u514d\u8d39\u7248";
tierEl.style.color = isPro ? "#166534" : "#9a3412";
tierEl.style.background = isPro ? "#f0fdf4" : "#fff7ed";
tierEl.style.borderColor = isPro ? "#86efac" : "#fdba74";
}
if (quotaEl) {
quotaEl.textContent = PANEL_FEATURE_SLOGAN;
quotaEl.title = "";
quotaEl.classList.add("jxjy-sub-chip-em");
}
if (expireEl) {
if (isPro && expireText) {
expireEl.textContent = `\u5230\u671f ${expireText}`;
expireEl.title = "";
} else if (isPro) {
expireEl.textContent = "Pro \u5df2\u5f00\u901a";
expireEl.title = "";
} else {
expireEl.textContent = `\u4f53\u9a8c ${state.freeUsedChapters}/${state.freeChapterLimit} \u7ae0\u8282`;
expireEl.title = "";
}
}
if (settingsTier) {
settingsTier.textContent = isPro ? "Pro" : "\u514d\u8d39\u7248";
}
if (settingsExpire) {
settingsExpire.textContent =
isPro && expireText ? expireText : isPro ? "\u5df2\u5f00\u901a" : "—";
}
if (buyBtn) {
buyBtn.style.display = "";
buyBtn.textContent = isPro ? "\u7eed\u8d39 / \u67e5\u770b Pro" : "\u5347\u7ea7 Pro";
}
const inp = document.getElementById("jxjy-cloud-token-input");
if (inp && getCloudToken() && !inp.value) inp.value = getCloudToken();
}
function _upn() {
const el = document.getElementById("jxjy-ann-text");
if (el) {
el.textContent = String(state.panelNotice || PANEL_NOTICE);
}
}
async function _lcc() {
try {
const cfg = await _crq(`${CLOUD_API_PREFIX}client-config`, "GET", null);
if (cfg.proBuyUrl) state.proBuyUrl = String(cfg.proBuyUrl).trim() || PRO_BUY_URL;
if (cfg.freeChapterLimit != null || cfg.freeVideoLimit != null) {
state.freeChapterLimit = Number(cfg.freeChapterLimit ?? cfg.freeVideoLimit) || 3;
}
if (cfg.finishTip) state.finishTip = String(cfg.finishTip);
if (cfg.panelNotice) state.panelNotice = String(cfg.panelNotice);
_ucp();
_upn();
} catch (_) {}
try {
const res = await _gj(CLOUD_API_BASE + `${CLOUD_API_PREFIX}panel-notice`, {
method: "GET",
headers: { Accept: "text/plain, */*" },
});
if (res.status >= 200 && res.status < 300) {
const text = String(res.text || "").trim();
if (text) {
state.panelNotice = text;
_upn();
}
}
} catch (_) {}
}
function updateAuthUi() {
const badge = document.getElementById("jxjy-auth-badge");
const ok = !!(state.student && (state.student.id || state.student.userid));
if (badge) {
badge.textContent = ok ? "\u5df2\u767b\u5f55" : state.loading ? "\u52a0\u8f7d\u4e2d" : "\u672a\u767b\u5f55";
badge.title = ok ? "\u5e73\u53f0\u767b\u5f55\u6709\u6548" : "\u8bf7\u5148\u767b\u5f55\u5e73\u53f0";
badge.style.color = ok ? "#166534" : "#92400e";
badge.style.borderColor = ok ? "#86efac" : "#fcd34d";
badge.style.background = ok ? "#f0fdf4" : "#fffbeb";
}
_ucp();
}
async function loadYearsAndCourses(opts = {}) {
const quiet = !!opts.quiet;
if (!quiet) state.loading = true;
updateAuthUi();
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));
syncSelectedCourseKeys();
renderYearSelect();
renderCourseList();
if (state.selectedCourseKeys.size) {
try {
await refreshChapterPreview({ quiet: true });
} catch (_) {}
} else {
updateSummary();
}
if (!quiet) {
_plog(
state.selectedYear
? `\u5df2\u52a0\u8f7d ${state.selectedYear} \u5e74 · ${state.courses.length} \u95e8\u8bfe\u7a0b`
: `\u5df2\u52a0\u8f7d ${state.years.length} \u4e2a\u5e74\u5ea6`
);
}
} catch (e) {
if (!quiet) _plog(e.message || String(e));
if (!state.student) {
renderYearSelect();
renderCourseList();
}
} finally {
state.loading = false;
updateAuthUi();
}
}
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 refreshChapterPreview(opts = {}) {
const quiet = !!opts.quiet;
const keys = [...state.selectedCourseKeys];
if (!keys.length) {
state.chapterPreview = [];
renderChapterPreview();
updateSummary();
return;
}
if (!quiet) {
const box = document.getElementById("jxjy-chapter-preview");
if (box) box.innerHTML = `\u52a0\u8f7d\u7ae0\u8282…
`;
}
const blocks = [];
for (const key of keys) {
const { year, cid } = parseCourseKey(key);
const course = findCourse(year, cid);
if (!course) continue;
try {
const detail = await fetchCourseDetail(cid);
const chapters = (detail.courseChapter || []).map((ch) => mapChapter(ch, cid));
const computed = computeChapterBlockProgress(chapters);
course.progress = computed;
recomputeYearProgress(year);
blocks.push({
year,
courseId: cid,
courseTitle: course.name,
yearLabel: `${year}\u5e74`,
courseProgress: computed,
computedProgress: computed,
chapters,
});
} catch (e) {
blocks.push({
year,
courseId: cid,
courseTitle: course.name,
yearLabel: `${year}\u5e74`,
error: e.message || String(e),
chapters: [],
});
}
}
state.chapterPreview = blocks;
renderChapterPreview();
renderCourseList();
renderYearSelect();
updateSummary();
}
function applyLiveProgress(course, chapter) {
const pct = Math.min(100, Math.max(0, Number(chapter.progress) || 0));
const year = Number(course.year);
const c = findCourse(year, course.id);
let block = state.chapterPreview.find(
(x) => Number(x.courseId) === Number(course.id) && Number(x.year) === year
);
if (!block) {
block = {
year,
courseId: course.id,
courseTitle: course.name,
yearLabel: `${year}\u5e74`,
courseProgress: c ? c.progress : 0,
computedProgress: 0,
chapters: [],
};
state.chapterPreview.push(block);
}
const chs = block.chapters || (block.chapters = []);
const idx = chs.findIndex((v) => Number(v.id) === Number(chapter.id));
const snap = {
...(idx >= 0 ? chs[idx] : {}),
id: chapter.id,
serial: chapter.serial,
name: chapter.name,
duration: chapter.duration,
position: chapter.position,
progress: pct,
done: chapter.done || pct >= COMPLETE_THRESHOLD,
status:
chapter.done || pct >= COMPLETE_THRESHOLD
? "completed"
: pct > 0
? "learning"
: "notstarted",
};
if (idx >= 0) chs[idx] = snap;
else chs.push(snap);
const computed = computeChapterBlockProgress(chs);
block.computedProgress = computed;
block.courseProgress = computed;
block.courseTitle = course.name;
if (c) c.progress = computed;
recomputeYearProgress(year);
renderCourseList();
renderChapterPreview();
updateYearProgressLabels();
updateSummary();
}
function formatProgressLine(course, chapter) {
const pct = Math.min(100, Number(chapter.progress) || 0);
return `${course.name}${chapter.name} \u5df2\u5b66${pct}%`;
}
async function _soc(chapter, course) {
if (chapter.done || (Number(chapter.progress) || 0) >= COMPLETE_THRESHOLD) {
return true;
}
setCurrentTask(`${truncate(course.name, 8)} / ${chapter.name}`);
applyLiveProgress(course, chapter);
let sessionId = "local";
const startRes = await _es({
courseId: course.id,
chapterId: chapter.id,
chapterSerial: chapter.serial,
year: course.year,
learning_user_id: getLearningUserId(),
site_id: SITE_ID,
duration: chapter.duration || 60,
position: chapter.position || 0,
progress: chapter.progress || 0,
courseName: course.name || "",
chapterName: chapter.name || "",
});
sessionId = startRes.session_id || "local";
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;
let lastResult = { ok: false };
while (!state.stopFlag && progress < COMPLETE_THRESHOLD && position < duration) {
const stepSec = TURBO_MODE ? TURBO_STEP_SEC : REPORT_INTERVAL;
const studylong = Math.min(stepSec, duration - position);
if (studylong <= 0) break;
await sleep(TURBO_MODE ? TURBO_DELAY_MS : studylong * 1000);
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";
_plog(formatProgressLine(course, chapter), course.name);
applyLiveProgress(course, chapter);
lastResult = { ok: true, progress, position, studylong };
} catch (e) {
lastResult = { ok: false, message: String(e.message || e) };
throw e;
}
}
if (state.stopFlag) {
try {
await _estep(sessionId, "stopped", lastResult);
} catch (_) {}
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";
applyLiveProgress(course, chapter);
try {
await _estep(sessionId, "chapter_done", lastResult);
} catch (e) {
if (chapter.done) {
_plog(`${course.name}${chapter.name}\u5b8c\u6210`, course.name);
return true;
}
throw e;
}
if (chapter.done) {
_plog(`${course.name}${chapter.name}\u5b8c\u6210`, course.name);
return true;
}
return false;
}
async function _scs(course) {
let guard = 0;
while (!state.stopFlag && guard++ < 80) {
const detail = await fetchCourseDetail(course.id);
const chapters = (detail.courseChapter || [])
.map((ch) => mapChapter(ch, course.id))
.sort((a, b) => (a.serial || 0) - (b.serial || 0));
const pending = chapters.filter(
(ch) => !ch.done && (Number(ch.progress) || 0) < COMPLETE_THRESHOLD
);
if (!pending.length) {
_plog(`${course.name}\u5b8c\u6210`, course.name);
course.progress = 100;
const block = state.chapterPreview.find(
(x) =>
Number(x.courseId) === Number(course.id) &&
Number(x.year) === Number(course.year)
);
if (block) {
block.courseProgress = 100;
block.computedProgress = 100;
(block.chapters || []).forEach((ch) => {
ch.progress = 100;
ch.done = true;
ch.status = "completed";
});
}
recomputeYearProgress(course.year);
renderCourseList();
renderChapterPreview();
updateYearProgressLabels();
updateSummary();
return;
}
const chapter = pending[0];
const ok = await _soc(chapter, course);
if (!ok && !state.stopFlag) {
_plog(`\u300c${course.name}\u300d\u672a\u5b8c\u6210\uff0c\u5df2\u505c\u6b62`);
return;
}
}
}
async function _rs() {
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) {
_plog("\u6ca1\u6709\u53ef\u5b66\u4e60\u7684\u8bfe\u7a0b");
return "no_courses";
}
try {
await refreshChapterPreview({ quiet: true });
} catch (_) {}
_plog(`\u5f00\u59cb\u5b66\u4e60 · ${courses.length} \u95e8\u8bfe`);
for (const course of courses) {
if (state.stopFlag) break;
try {
await _scs(course);
} catch (e) {
if (_ifqe(e)) return "quota_exceeded";
_plog(`\u8bfe\u7a0b\u4e2d\u65ad\u300c${course.name}\u300d\uff1a${e.message || e}`);
return "error";
}
}
if (state.stopFlag) return "stopped";
return "completed";
}
async function _ss() {
if (state.running) return;
if (!state.student) {
_plog("\u8bf7\u5148\u767b\u5f55\u5e73\u53f0");
return;
}
if (state.selectedYear == null) {
_plog("\u8bf7\u5148\u9009\u62e9\u57f9\u8bad\u5e74\u5ea6");
return;
}
if (!state.selectedCourseKeys.size) {
_plog("\u8bf7\u5148\u9009\u62e9\u8981\u5b66\u4e60\u7684\u8bfe\u7a0b");
return;
}
state.running = true;
state.stopFlag = false;
syncRunButtons();
setCurrentTask("\u5b66\u4e60\u4e2d…");
showLogTab();
try {
await _cl(true);
if (_ifqx()) {
await _hfq();
return;
}
const result = await _rs();
if (result === "completed" && !state.stopFlag) {
_plog("\u5168\u90e8\u8bfe\u7a0b\u5df2\u5b66\u5b8c");
if (state.finishTip) {
await _spa(String(state.finishTip), "\u5b66\u4e60\u5b8c\u6210");
}
} else if (result === "quota_exceeded") {
await _hfq();
}
} catch (e) {
if (_ifqe(e)) {
await _hfq();
} else {
_plog(`\u770b\u8bfe\u4e2d\u65ad\uff1a${e.message || e}`);
}
} finally {
state.running = false;
state.stopFlag = false;
syncRunButtons();
setCurrentTask("\u5f85\u547d");
try {
await loadYearsAndCourses({ quiet: true });
} catch (_) {}
}
}
function _st() {
if (!state.running) return;
state.stopFlag = true;
setCurrentTask("\u6b63\u5728\u505c\u6b62…");
}
function updateSummary() {
let total = 0;
let done = 0;
let learnedSec = 0;
let totalSec = 0;
state.chapterPreview.forEach((c) => {
(c.chapters || []).forEach((ch) => {
total += 1;
if (ch.done) done += 1;
totalSec += Number(ch.duration) || 0;
learnedSec += ((Number(ch.duration) || 0) * (Number(ch.progress) || 0)) / 100;
});
});
const pct =
totalSec > 0
? Math.round((learnedSec / totalSec) * 100)
: total
? Math.round((done / total) * 100)
: 0;
const selCount = state.selectedCourseKeys.size;
const doneEl = document.getElementById("jxjy-queue-done");
const totalEl = document.getElementById("jxjy-queue-total");
const pctEl = document.getElementById("jxjy-queue-percent");
const bar = document.getElementById("jxjy-queue-progress");
const selEl = document.getElementById("jxjy-selected-count");
const foot = document.getElementById("jxjy-footer-text");
if (selEl) selEl.textContent = String(selCount);
if (doneEl) doneEl.textContent = String(done);
if (totalEl) totalEl.textContent = String(total);
if (pctEl) pctEl.textContent = `${pct}%`;
if (bar) bar.style.width = `${pct}%`;
if (foot) {
foot.textContent = state.selectedYear
? `${state.selectedYear}\u5e74 · ${state.courses.length} \u95e8\u8bfe · \u5df2\u9009 ${selCount} \u95e8`
: state.years.length
? "\u8bf7\u9009\u62e9\u57f9\u8bad\u5e74\u5ea6"
: "\u8bf7\u767b\u5f55\u540e\u52a0\u8f7d\u8bfe\u7a0b";
}
}
function updateYearProgressLabels() {
const sel = document.getElementById("jxjy-year-select");
if (!sel) return;
[...sel.options].forEach((opt) => {
const y = Number(opt.value);
const meta = state.years.find((x) => Number(x.year) === y);
const count =
Number(y) === Number(state.selectedYear)
? state.courses.length
: state.courses.filter((c) => Number(c.year) === y).length;
if (meta) opt.textContent = `${y}\u5e74 · ${meta.progress || 0}% · ${count}\u8bfe`;
});
}
function renderYearSelect() {
const wrap = document.getElementById("jxjy-year-select-wrap");
if (!wrap) return;
if (!state.years.length) {
wrap.innerHTML = `${state.student ? "\u6682\u65e0\u57f9\u8bad\u5e74\u5ea6" : "\u767b\u5f55\u540e\u52a0\u8f7d\u57f9\u8bad"}
`;
return;
}
const cur = state.selectedYear ?? state.years[0].year;
wrap.innerHTML = ``;
wrap.querySelector("#jxjy-year-select").addEventListener("change", async (e) => {
state.selectedYear = Number(e.target.value);
state.selectedCourseKeys = new Set();
await loadYearsAndCourses({ quiet: true });
});
}
function toggleIncompleteCourses() {
const year = state.selectedYear;
const incomplete = state.courses
.filter(
(c) =>
Number(c.year) === Number(year) &&
(Number(c.progress) || 0) < COMPLETE_THRESHOLD
)
.map((c) => courseKey(c.year, c.id));
if (!incomplete.length) {
_plog("\u5f53\u524d\u5e74\u5ea6\u6ca1\u6709\u672a\u5b8c\u6210\u8bfe\u7a0b");
return;
}
const allChecked = incomplete.every((k) => state.selectedCourseKeys.has(k));
if (allChecked) {
incomplete.forEach((k) => state.selectedCourseKeys.delete(k));
} else {
incomplete.forEach((k) => state.selectedCourseKeys.add(k));
}
renderCourseList();
updateSummary();
refreshChapterPreview();
}
function renderCourseList() {
const box = document.getElementById("jxjy-course-list");
if (!box) return;
if (state.selectedYear == null) {
box.innerHTML = `\u8bf7\u5148\u9009\u62e9\u57f9\u8bad\u5e74\u5ea6
`;
return;
}
const year = state.selectedYear;
const list = state.courses.filter((c) => Number(c.year) === Number(year));
if (!list.length) {
box.innerHTML = `\u8be5\u5e74\u5ea6\u6682\u65e0\u8bfe\u7a0b
`;
return;
}
box.innerHTML = list
.map((c) => {
const pct = Math.min(100, Math.max(0, c.progress || 0));
const key = courseKey(c.year, c.id);
const checked = state.selectedCourseKeys.has(key) ? "checked" : "";
const active = checked ? "active" : "";
const done = pct >= COMPLETE_THRESHOLD;
const cc = Number(c.chapter_count) || 0;
const doneCh = Number(c.completed_count) || 0;
const sub =
cc > 0
? `${c.credit || 0} \u5b66\u65f6 · ${doneCh}/${cc}\u7ae0 · ${pct}%`
: `${c.credit || 0} \u5b66\u65f6 · ${pct}%`;
return `
`;
})
.join("");
box.querySelectorAll("input[type=checkbox]").forEach((el) => {
el.addEventListener("change", () => {
const cur = [];
box.querySelectorAll("input[type=checkbox]").forEach((cb) => {
if (cb.checked) cur.push(cb.dataset.ckey);
});
state.selectedCourseKeys = new Set(cur);
renderCourseList();
updateSummary();
refreshChapterPreview();
});
});
}
function renderChapterPreview() {
const box = document.getElementById("jxjy-chapter-preview");
if (!box) return;
if (!state.chapterPreview.length) {
box.innerHTML = `\u8bf7\u9009\u62e9\u8bfe\u7a0b\u67e5\u770b\u7ae0\u8282
`;
return;
}
box.innerHTML = state.chapterPreview
.map((course) => {
if (course.error) {
return `${escHtml(course.yearLabel || "")} · ${escHtml(course.courseTitle)}
${escHtml(course.error)}
`;
}
const items = (course.chapters || [])
.map((ch) => {
const cls = ch.done ? "done" : ch.status === "learning" ? "learning" : "";
const mark = ch.done ? "✓" : ch.status === "learning" ? "▶" : "•";
return `${mark} ${escHtml(ch.serial ? `${ch.serial}. ` : "")}${escHtml(ch.name)} ${formatTime(ch.duration)}${Math.round(ch.progress)}%
`;
})
.join("");
return `${escHtml(course.yearLabel || "")} · ${escHtml(course.courseTitle)} · \u8bfe ${course.courseProgress}% · \u7ae0\u8282\u5408\u8ba1 ${course.computedProgress ?? 0}%
${items || `
\u65e0\u7ae0\u8282
`}
`;
})
.join("");
}
function injectStyles() {
if (document.getElementById("jxjy-panel-style")) return;
const st = document.createElement("style");
st.id = "jxjy-panel-style";
st.textContent = `
#jxjy-auto-panel{position:fixed;right:20px;top:80px;z-index:999999;width:412px;display:flex;flex-direction:column;background:#f4f7fb;border:1px solid #dbe4f0;border-radius:16px;box-shadow:0 16px 40px rgba(15,23,42,.17);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;font-size:12px;color:#0f172a;overflow:hidden;transition:max-height .22s ease,box-shadow .22s ease;}
#jxjy-auto-panel.jxjy-panel-max{max-height:min(92vh,780px);}
#jxjy-auto-panel.jxjy-panel-min{max-height:none;box-shadow:0 10px 28px rgba(15,23,42,.14);}
#jxjy-auto-panel.jxjy-panel-min #jxjy-panel-body,#jxjy-auto-panel.jxjy-panel-min #jxjy-panel-footer,#jxjy-auto-panel.jxjy-panel-min .jxjy-footer-extra{display:none !important;}
#jxjy-auto-panel.jxjy-panel-min #jxjy-panel-header{border-bottom:none;}
#jxjy-panel-header{flex:0 0 auto;padding:8px 11px;background:linear-gradient(180deg,#f8ecd5,#f4e8cf);border-bottom:1px solid #e5dbc6;display:flex;justify-content:space-between;align-items:center;cursor:move;user-select:none;}
#jxjy-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;}
#jxjy-panel-logo{width:30px;height:30px;border-radius:9px;display:block;object-fit:cover;background:#fff;border:1px solid rgba(148,163,184,.45);box-shadow:0 2px 9px rgba(15,23,42,.11);flex:0 0 auto;}
#jxjy-panel-title{font-size:13px;font-weight:900;color:#9a3412;line-height:1.26;display:flex;align-items:flex-start;gap:5px;flex-wrap:wrap;}
.jxjy-panel-title-text{flex:1 1 12em;min-width:0;}
.jxjy-panel-version{font-size:11px;font-weight:900;color:#64748b;padding:2px 7px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0;}
#jxjy-panel-sub{display:flex;flex-wrap:wrap;gap:3px 5px;margin-top:3px;}
.jxjy-sub-chip{font-size:11px;color:#7c2d12;background:rgba(255,255,255,.62);padding:2px 7px;border-radius:999px;border:1px solid rgba(180,83,9,.18);font-weight:700;}
.jxjy-sub-chip-em{color:#0f766e;background:rgba(236,253,245,.9);border-color:rgba(15,118,110,.28);}
.jxjy-sub-chip-mute{color:#57534e;background:rgba(255,255,255,.55);border-color:rgba(120,113,108,.22);}
#jxjy-panel-controls{display:flex;gap:5px;}
.jxjy-panel-ctl{border:none;background:#fff;color:#64748b;width:28px;height:28px;border-radius:999px;cursor:pointer;box-shadow:0 1px 2px rgba(15,23,42,.1);font-weight:900;}
#jxjy-panel-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;}
.jxjy-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:7px;}
.jxjy-card-status{padding:6px 8px;margin-bottom:6px;}
.jxjy-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;}
.jxjy-status-label{font-size:11px;color:#64748b;font-weight:700;}
.jxjy-status-badge{padding:2px 7px;border-radius:999px;font-weight:800;font-size:11px;background:#fff;border:1px solid #cbd5e1;color:#334155;}
.jxjy-status-main{margin-top:3px;}
.jxjy-status-metrics{display:flex;align-items:center;gap:5px;font-size:12px;color:#475569;min-width:0;flex:1;}
.jxjy-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;}
.jxjy-progress-pct{font-size:14px;font-weight:900;color:#0369a1;flex:0 0 auto;}
.jxjy-progress-bar{height:5px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:4px;}
.jxjy-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s ease;}
.jxjy-tabbar{display:flex;gap:6px;margin-bottom:7px;}
.jxjy-tab-btn{flex:1;border:1px solid #cbd5e1;background:#f8fafc;color:#475569;padding:4px;border-radius:9px;cursor:pointer;font-weight:700;font-size:11px;}
.jxjy-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;}
.jxjy-pane{display:none;}.jxjy-pane.active{display:block;}
.jxjy-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;gap:6px;}
.jxjy-list-head-actions{display:flex;align-items:center;gap:5px;flex:0 0 auto;}
.jxjy-mini-btn{border:1px solid #cbd5e1;background:#fff;color:#334155;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:700;cursor:pointer;line-height:1.4;white-space:nowrap;}
.jxjy-mini-btn:hover{background:#f8fafc;color:#0f172a;}
.jxjy-mini-btn.on{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8;}
.jxjy-list-title{font-size:12px;color:#64748b;font-weight:700;}
.jxjy-list-tag{font-size:11px;color:#92400e;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:2px 7px;}
.jxjy-year-select{width:100%;padding:6px 9px;border:1px solid #cbd5e1;border-radius:9px;font-size:12px;font-weight:700;color:#334155;background:#fff;margin-bottom:7px;box-sizing:border-box;}
.jxjy-year-select:focus{outline:none;border-color:#2563eb;box-shadow:0 0 0 2px rgba(37,99,235,.15);}
#jxjy-year-select-wrap{margin-bottom:4px;}
#jxjy-course-list,#jxjy-chapter-preview,#jxjy-run-log{max-height:220px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px;}
.jxjy-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:900;}
.jxjy-course-item{display:flex;gap:7px;align-items:flex-start;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:7px;margin-bottom:6px;cursor:pointer;}
.jxjy-course-item.done{opacity:.92;background:#f1f5f9;}
.jxjy-course-item.active{border-color:#16a34a;background:#f0fdf4;}
.jxjy-bar{display:block;height:4px;background:#e2e8f0;border-radius:2px;margin-top:5px;overflow:hidden;}
.jxjy-bar>i{display:block;height:100%;background:#2563eb;border-radius:2px;}
.jxjy-bar>i.full{background:#16a34a;}
.jxjy-chapter-course{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff;}
.jxjy-chapter-title{padding:6px 9px;background:#eaf1ff;border-bottom:1px solid #dbe4f0;color:#1d4ed8;font-size:12px;font-weight:700;}
.jxjy-chapter-item{padding:5px 9px;font-size:12px;display:flex;justify-content:space-between;gap:7px;color:#334155;}
.jxjy-chapter-item.done{color:#64748b;}
.jxjy-chapter-item.learning{color:#0369a1;font-weight:600;}
.jxjy-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:12px;margin-bottom:5px;align-items:center;}
.jxjy-meta-label{color:#64748b;}
.jxjy-meta-value{font-weight:700;text-align:right;}
.jxjy-btn-row{display:flex;gap:7px;margin-top:6px;}
.jxjy-btn{flex:1;border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px;}
.jxjy-btn-primary{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);}
.jxjy-btn-start{background:#16a34a;}
.jxjy-btn-stop{background:#ef4444;}
.jxjy-btn-off,.jxjy-btn:disabled{background:#e2e8f0 !important;color:#94a3b8 !important;cursor:not-allowed;box-shadow:none;}
.jxjy-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#0f172a;}
.jxjy-task-row{font-size:11px;color:#475569;margin-top:6px;line-height:1.35;}
.jxjy-task-row em{font-style:normal;font-weight:800;color:#0f172a;}
.jxjy-input{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:6px 8px;font-size:12px;box-sizing:border-box;}
.jxjy-footer-extra{flex:0 0 auto;padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;}
.jxjy-ann{position:relative;font-size:12px;color:#334155;line-height:1.42;background:linear-gradient(180deg,#fffdf5,#fff7e6);border:1px solid #fcd34d;border-radius:11px;padding:8px 9px 8px 11px;}
.jxjy-ann::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:linear-gradient(180deg,#f59e0b,#ef4444);border-radius:11px 0 0 11px;}
#jxjy-panel-footer{flex:0 0 auto;padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;}
.jxjy-log-row{padding:4px 7px 4px 8px;margin-bottom:2px;border-bottom:1px dashed #e2e8f0;border-left:3px solid #94a3b8;border-radius:0 7px 7px 0;font-size:12px;line-height:1.42;background:#fff;}
.jxjy-log-time{color:#94a3b8;font-weight:500;margin-right:2px;}
.jxjy-log-pct{font-weight:700;text-decoration:underline;text-underline-offset:2px;}
.jxjy-log-mins{opacity:.88;}
.jxjy-log-done{background:rgba(16,185,129,.1);}
.jxjy-log-done-mark{font-weight:700;color:#059669;}
.jxjy-log-info{background:#f8fafc;color:#64748b!important;border-left-color:#94a3b8!important;}
.jxjy-log-progress{background:color-mix(in srgb, var(--log-c) 7%, #fff);}
#jxjy-dialog-modal{position:fixed;inset:0;background:rgba(15,23,42,.45);z-index:1000002;display:none;align-items:center;justify-content:center;padding:20px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;}
.jxjy-dialog-card{width:min(420px,92vw);background:#fff;border-radius:16px;border:1px solid #dbe4f0;box-shadow:0 18px 46px rgba(15,23,42,.25);overflow:hidden;}
.jxjy-dialog-head{padding:12px 16px 10px;background:linear-gradient(180deg,#f8ecd5,#f4e8cf);border-bottom:1px solid #e5dbc6;}
.jxjy-dialog-title{font-size:16px;font-weight:900;color:#9a3412;line-height:1.35;}
.jxjy-dialog-body{padding:14px 16px 16px;font-size:13px;color:#475569;line-height:1.55;white-space:pre-wrap;word-break:break-word;}
.jxjy-dialog-actions{display:flex;gap:8px;padding:0 16px 16px;}
.jxjy-dialog-actions .jxjy-btn{flex:1;}
`;
document.head.appendChild(st);
}
let panelDialogResolve = null;
function _cpd(result) {
const modal = document.getElementById("jxjy-dialog-modal");
if (modal) modal.style.display = "none";
if (panelDialogResolve) {
const done = panelDialogResolve;
panelDialogResolve = null;
done(result);
}
}
function _epd() {
injectStyles();
let modal = document.getElementById("jxjy-dialog-modal");
if (modal) return modal;
modal = document.createElement("div");
modal.id = "jxjy-dialog-modal";
modal.innerHTML = `
`;
document.body.appendChild(modal);
modal.addEventListener("click", (e) => {
if (e.target === modal && modal.dataset.dismissible === "1") _cpd(true);
});
return modal;
}
function _spa(message, title) {
return new Promise((resolve) => {
const modal = _epd();
modal.dataset.dismissible = "1";
modal.querySelector("#jxjy-dialog-title").textContent = title || "\u63d0\u793a";
modal.querySelector("#jxjy-dialog-body").textContent = String(message || "");
const actions = modal.querySelector("#jxjy-dialog-actions");
actions.innerHTML =
'';
panelDialogResolve = () => resolve(true);
actions.querySelector("#jxjy-dialog-ok").onclick = () => _cpd(true);
modal.style.display = "flex";
});
}
function _spc(message, opts) {
const o = opts || {};
return new Promise((resolve) => {
const modal = _epd();
modal.dataset.dismissible = "0";
modal.querySelector("#jxjy-dialog-title").textContent = o.title || "\u786e\u8ba4";
modal.querySelector("#jxjy-dialog-body").textContent = String(message || "");
const actions = modal.querySelector("#jxjy-dialog-actions");
actions.innerHTML = `
`;
panelDialogResolve = resolve;
actions.querySelector("#jxjy-dialog-cancel").onclick = () => _cpd(false);
actions.querySelector("#jxjy-dialog-confirm").onclick = () => _cpd(true);
modal.style.display = "flex";
});
}
function bindDrag(panel) {
const header = panel.querySelector("#jxjy-panel-header");
let sx = 0,
sy = 0,
ox = 0,
oy = 0,
dragging = false;
header.addEventListener("mousedown", (e) => {
if (e.target.closest("#jxjy-panel-controls")) return;
dragging = true;
sx = e.clientX;
sy = e.clientY;
const rect = panel.getBoundingClientRect();
ox = rect.left;
oy = rect.top;
e.preventDefault();
});
window.addEventListener("mousemove", (e) => {
if (!dragging) return;
const left = Math.max(0, ox + e.clientX - sx);
const top = Math.max(0, oy + e.clientY - sy);
panel.style.left = left + "px";
panel.style.top = top + "px";
panel.style.right = "auto";
});
window.addEventListener("mouseup", () => {
if (!dragging) return;
dragging = false;
try {
GM_setValue(PANEL_POS_KEY, { left: panel.style.left, top: panel.style.top });
} catch (_) {}
});
}
function createPanel() {
document.querySelectorAll("#jxjy-auto-panel").forEach((el) => el.remove());
injectStyles();
const panel = document.createElement("div");
panel.id = "jxjy-auto-panel";
panel.className = "jxjy-panel-max";
panel.innerHTML = `
\u5f53\u524d\u8bfe\u7a0b\u8fdb\u5ea6
\u672a\u767b\u5f55
\u5df2\u9009 0 \u95e8
·
\u7ae0\u8282 0/0
0%
\u9009\u62e9\u5e74\u5ea6\u4e0e\u8bfe\u7a0b
\u767b\u5f55\u540e\u52a0\u8f7d\u57f9\u8bad
\u767b\u5f55\u540e\u81ea\u52a8\u52a0\u8f7d\u8bfe\u7a0b
\u7ae0\u8282\u9884\u89c8\u7ae0\u8282\u8fdb\u5ea6
\u8bf7\u9009\u62e9\u8bfe\u7a0b
\u8fd0\u884c\u65e5\u5fd7
\u4e91\u7aef Pro\u6388\u6743
\u6863\u4f4d\u514d\u8d39\u7248
\u5230\u671f—
${
QQ_GROUP_URL
? `
`
: ""
}
\u5f53\u524d\u4efb\u52a1\uff1a\u5f85\u547d
`;
document.body.appendChild(panel);
try {
const pos = GM_getValue(PANEL_POS_KEY);
if (pos?.left && pos?.top) {
panel.style.left = pos.left;
panel.style.top = pos.top;
panel.style.right = "auto";
}
if (GM_getValue(PANEL_COLLAPSED_KEY)) {
panel.classList.add("jxjy-panel-min");
panel.classList.remove("jxjy-panel-max");
panel.querySelector("#jxjy-btn-min").style.display = "none";
panel.querySelector("#jxjy-btn-max").style.display = "";
}
} catch (_) {}
bindDrag(panel);
panel.querySelector("#jxjy-btn-min").onclick = () => {
panel.classList.add("jxjy-panel-min");
panel.classList.remove("jxjy-panel-max");
panel.querySelector("#jxjy-btn-min").style.display = "none";
panel.querySelector("#jxjy-btn-max").style.display = "";
try {
GM_setValue(PANEL_COLLAPSED_KEY, true);
} catch (_) {}
};
panel.querySelector("#jxjy-btn-max").onclick = () => {
panel.classList.remove("jxjy-panel-min");
panel.classList.add("jxjy-panel-max");
panel.querySelector("#jxjy-btn-min").style.display = "";
panel.querySelector("#jxjy-btn-max").style.display = "none";
try {
GM_setValue(PANEL_COLLAPSED_KEY, false);
} catch (_) {}
};
panel.querySelectorAll(".jxjy-tab-btn").forEach((btn) => {
btn.addEventListener("click", () => {
panel.querySelectorAll(".jxjy-tab-btn").forEach((b) => b.classList.remove("active"));
panel.querySelectorAll(".jxjy-pane").forEach((p) => p.classList.remove("active"));
btn.classList.add("active");
panel.querySelector(`.jxjy-pane[data-pane="${btn.dataset.tab}"]`)?.classList.add("active");
});
});
panel.querySelector("#jxjy-toggle-incomplete").onclick = () => toggleIncompleteCourses();
panel.querySelector("#jxjy-start").onclick = () => _ss();
panel.querySelector("#jxjy-stop").onclick = () => _st();
panel.querySelector("#jxjy-refresh-courses").onclick = () => loadYearsAndCourses();
panel.querySelector("#jxjy-buy-pro")?.addEventListener("click", () =>
openUrl(state.proBuyUrl || PRO_BUY_URL)
);
panel.querySelector("#jxjy-qq-group")?.addEventListener("click", () => openUrl(QQ_GROUP_URL));
panel.querySelector("#jxjy-save-cloud-token")?.addEventListener("click", async () => {
const inp = panel.querySelector("#jxjy-cloud-token-input");
setCloudToken(inp ? inp.value : "");
state.tokenBoundBlocked = false;
_icc();
try {
await _cl(true);
_plog(
state.cloudTier === "pro"
? "Pro Token \u9a8c\u8bc1\u6210\u529f"
: `\u5df2\u7528 ${state.freeUsedChapters}/${state.freeChapterLimit} \u7ae0\u8282`
);
} catch (e) {
_plog(`\u4e91\u7aef\u9a8c\u8bc1\u5931\u8d25\uff1a${e.message || e}`);
}
_ucp();
});
panel.querySelector("#jxjy-clear-log").onclick = () => {
state.logLines = [];
const box = document.getElementById("jxjy-run-log");
if (box) box.innerHTML = `\u6682\u65e0\u65e5\u5fd7
`;
};
syncRunButtons();
setCurrentTask("\u5f85\u547d");
updateAuthUi();
_upn();
}
function init() {
document.querySelectorAll("#jxjy-auto-panel").forEach((el) => el.remove());
getCloudToken();
createPanel();
updateAuthUi();
_plog(`\u5df2\u9002\u914d ${SITE_NAME}\uff08${WEB_SITE}\uff09`);
setTimeout(async () => {
try {
await _lcc();
await _cl(false);
} catch (_) {}
try {
await loadYearsAndCourses({ quiet: true });
} catch (_) {}
}, 600);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();