// ==UserScript==
// @name 华医网助手(自动下一课增强版)v3.2.0
// @namespace http://tampermonkey.net/
// @version 3.2.0
// @description 自动播放、跳过弹窗、精准跳转下一课(加强鲁棒性,适配多种DOM与文案)
// @author Yik Liu (Enhanced)
// @match *://*.91huayi.com/course_ware/course_ware_polyv.aspx?*
// @match *://*.91huayi.com/course_ware/course_list.aspx?*
// @match https://cme28.91huayi.com/pages/exam_result.aspx?cwid=*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
let examErrorCount = 0;
const CONTACT_IMG_SIZE = 180;
// ===== 限刷计数:每刷三节就停止 =====
const LIMIT_KEY = 'HY_CLASS_LIMIT_COUNT';
const LIMIT_MAX = 3;
const Limit = {
get() { try { return parseInt(sessionStorage.getItem(LIMIT_KEY) || '0', 10); } catch { return 0; } },
set(v) { try { sessionStorage.setItem(LIMIT_KEY, String(v)); } catch {} },
inc() { const n = this.get() + 1; this.set(n); updateLimitDisplay(); return n; },
reached() { return this.get() >= LIMIT_MAX; },
reset() { this.set(0); updateLimitDisplay(); }
};
// 如果是用户刷新(reload),则重置计数;正常页面跳转(navigate)不重置
try {
const nav = (performance.getEntriesByType && performance.getEntriesByType('navigation')[0]) || null;
if (nav && nav.type === 'reload') {
Limit.reset();
}
} catch {}
function updateLimitDisplay() {
const el = document.getElementById('h-limit');
if (el) el.innerText = `限刷: ${Limit.get()}/${LIMIT_MAX}`;
}
function createStatusPanel() {
if (document.getElementById('huayi-status')) return;
const panel = document.createElement('div');
panel.id = 'huayi-status';
panel.style.cssText = `
position: fixed; right: 10px; bottom: 10px; z-index: 999999;
background: rgba(0,0,0,0.75); color: #fff; padding: 10px 12px;
border-radius: 10px; font-size: 14px; max-width: 300px; line-height: 1.4;
backdrop-filter: blur(2px);
`;
panel.innerHTML = `
<div><b>华医网助手状态 (v3.2.0)</b></div>
<div id="h-status">状态: 启动中</div>
<div id="h-action">操作: -</div>
<div id="h-exam">考试按钮: -</div>
<div id="h-error">异常检测: 0 次</div>
<div id="h-title">当前视频: -</div>
<div id="h-limit">限刷: 0/${LIMIT_MAX}</div>
<div style="margin-top:6px; display:flex; gap:6px;">
<button id="h-next" style="padding:4px 8px;border-radius:6px;border:0;background:#00a0f6;color:#fff;cursor:pointer;">强制下一课</button>
<button id="h-hide" style="padding:4px 8px;border-radius:6px;border:0;background:#666;color:#fff;cursor:pointer;">隐藏面板</button>
<button id="h-reset" style="padding:4px 8px;border-radius:6px;border:0;background:#999;color:#fff;cursor:pointer;">清零</button>
</div>
`;
document.body.appendChild(panel);
document.getElementById('h-next').onclick = () => autoJumpToLearningVideo();
document.getElementById('h-hide').onclick = () => panel.remove();
document.getElementById('h-reset').onclick = () => Limit.reset();
updateLimitDisplay();
}
function updateStatusPanel(status, action, exam, errors, title) {
const set = (id, text) => {
const el = document.getElementById(id);
if (el) el.innerText = text;
};
set('h-status', `状态: ${status}`);
set('h-action', `操作: ${action}`);
set('h-exam', `考试按钮: ${exam}`);
set('h-error', `异常检测: ${errors} 次`);
set('h-title', `当前视频: ${title}`);
}
function autoSkipPopup() {
const tryClose = () => {
try {
document.querySelector('.pv-ask-skip')?.click();
document.querySelector('.signBtn')?.click();
document.querySelector("button[onclick='closeProcessbarTip()']")?.click();
document.querySelector('button.btn_sign')?.click();
if (document.querySelector('#floatTips')?.style.display !== 'none') {
window.closeFloatTips?.();
}
document.querySelector('.el-message-box__btns .el-button--primary')?.click();
document.querySelector('.el-dialog__footer .el-button--primary')?.click();
} catch (e) {}
};
tryClose();
setInterval(tryClose, 2000);
}
function autoPlayVideo() {
const video = document.querySelector('video');
if (!video) return;
video.muted = true;
video.volume = 0;
const ensurePlay = () => {
if (video.paused) video.play().catch(() => {});
};
ensurePlay();
const playInterval = setInterval(ensurePlay, 1000);
video.addEventListener('play', () => {
updateStatusPanel('监控中', '播放中', '检测中', examErrorCount, document.title);
});
document.addEventListener('visibilitychange', () => {
if (!document.hidden) ensurePlay();
});
attachEndedFallback();
}
function attachEndedFallback() {
const v = document.querySelector('video');
if (!v) return;
v.addEventListener(
'ended',
() => {
setTimeout(autoJumpToLearningVideo, 1000);
},
{ once: true }
);
}
function toAbsoluteUrl(maybeUrl) {
if (!maybeUrl) return '';
if (/^https?:\/\//i.test(maybeUrl)) return maybeUrl;
if (/^javascript:/i.test(maybeUrl)) return '';
const a = document.createElement('a');
a.href = maybeUrl;
return a.href;
}
function extractUrlFromOnclick(onclickStr) {
if (!onclickStr) return '';
let m =
onclickStr.match(/(['"])(https?:\/\/[^'"]*course_ware[^'"]*\.aspx\?[^'"]*)\1/i) ||
onclickStr.match(/(['"])(https?:\/\/[^'"]*course_ware[^'"]*\.aspx)\1/i);
if (m) return m[2] || m[1];
m =
onclickStr.match(/(course_ware[^'"]*\.aspx\?[^'"]*)/i) ||
onclickStr.match(/(course_ware[^'"]*\.aspx)/i);
if (m) return m[1];
return '';
}
function autoJumpToLearningVideo() {
if (Limit.reached()) {
updateStatusPanel('已暂停', '已达到限刷上限', '-', examErrorCount, document.title);
return;
}
updateStatusPanel('跳转中', '查找下一个课程...', '-', examErrorCount, document.title);
const docs = [document];
try {
if (window.top && window.top !== window && window.top.document) {
docs.push(window.top.document);
}
} catch (e) {}
const itemSelectors = [
'li.lis-inside-content',
'li.lis_content',
'.lis-inside-content li',
'.lis-content li',
'li[class*="lis"]'
];
const nextableTexts = ['学习中', '继续学习', '立即学习'];
const doneOrExamTexts = ['待考试', '已完成', '考试', '合格'];
const currentCwid = new URLSearchParams(window.location.search).get('cwid');
let items = [];
for (const d of docs) {
for (const sel of itemSelectors) {
const found = Array.from(d.querySelectorAll(sel));
if (found.length) {
items = found;
break;
}
}
if (items.length) break;
}
if (!items.length) {
updateStatusPanel('错误', '未找到课程列表', '-', examErrorCount, document.title);
return;
}
const isNextable = (el) => {
const btn = el.querySelector('button, a, .state_btn, .state_lis_btn, [role="button"], input[type="button"], input[type="submit"]');
const text = (btn?.innerText || btn?.value || el.innerText || '').replace(/\s/g, '');
if (nextableTexts.some((t) => text.includes(t))) return true;
if (btn) {
const cs = getComputedStyle(btn);
const bg = cs.backgroundColor || cs.background;
if (/rgb\s*\(\s*0\s*,\s*160\s*,\s*246\s*\)/i.test(bg)) return true;
if (/#00a0f6/i.test(cs.background || '')) return true;
}
return false;
};
let currentIndex = -1;
if (currentCwid) {
currentIndex = items.findIndex((item) => {
const cand = [
item.querySelector('h2[onclick]'),
item.querySelector('[onclick*="cwid="]'),
item.querySelector('a[href*="cwid="]')
].filter(Boolean);
return cand.some((el) => {
const s = el.getAttribute('onclick') || el.getAttribute('href') || '';
return s.includes(`cwid=${currentCwid}`);
});
});
}
const start = currentIndex !== -1 ? currentIndex + 1 : 0;
let nextTarget = null;
for (let i = start; i < items.length; i++) {
const rawText = (items[i].innerText || '').replace(/\s/g, '');
if (doneOrExamTexts.some((t) => rawText.includes(t))) continue;
if (isNextable(items[i])) {
nextTarget = items[i];
break;
}
}
if (!nextTarget) {
for (let i = start; i < items.length; i++) {
const rawText = (items[i].innerText || '').replace(/\s/g, '');
if (/未学习|去学习|开始学习/.test(rawText)) {
nextTarget = items[i];
break;
}
}
}
if (!nextTarget) {
updateStatusPanel('待命', '无更多课程可学习', '-', examErrorCount, document.title);
return;
}
const urlCands = [
nextTarget.querySelector('a[href*="course_ware_polyv.aspx"]'),
nextTarget.querySelector('[onclick*="course_ware_polyv.aspx"]'),
nextTarget.querySelector('a[href*="course_ware.aspx"]'),
nextTarget.querySelector('[onclick*="course_ware.aspx"]')
].filter(Boolean);
let targetUrl = '';
for (const el of urlCands) {
const href = el.getAttribute('href') || '';
const onclick = el.getAttribute('onclick') || '';
if (/course_ware.*\.aspx/i.test(href)) {
targetUrl = href;
} else {
const u = extractUrlFromOnclick(onclick);
if (u) targetUrl = u;
}
if (targetUrl) break;
}
if (targetUrl) {
targetUrl = toAbsoluteUrl(targetUrl);
if (targetUrl) {
const count = Limit.inc();
updateStatusPanel('跳转中', `第 ${count}/${LIMIT_MAX} 节,准备跳转`, '-', examErrorCount, document.title);
location.href = targetUrl;
return;
}
}
const clickable = nextTarget.querySelector('h2[onclick], a, button, [role="button"], input[type="button"], input[type="submit"]');
if (clickable) {
const count = Limit.inc();
updateStatusPanel('跳转中', `第 ${count}/${LIMIT_MAX} 节,未抓到URL,尝试点击元素`, '-', examErrorCount, document.title);
try {
clickable.dispatchEvent(new MouseEvent('click', { bubbles: true }));
} catch (e) {
clickable.click?.();
}
} else {
updateStatusPanel('错误', '找到课程但无法点击或提取URL', '-', examErrorCount, document.title);
}
}
function observeStatusChange() {
const targetNode =
document.querySelector('.lis-content') ||
document.querySelector('.lis-inside-content') ||
document.body;
if (!targetNode) return;
const observer = new MutationObserver((mutationsList) => {
for (const m of mutationsList) {
if (m.type === 'characterData') {
const btn = m.target?.parentElement;
if (btn && /button|a/i.test(btn.tagName)) {
const t = (btn.innerText || '').replace(/\s/g, '');
if (t.includes('待考试') || t.includes('已完成')) {
updateStatusPanel('监控中', '课程完成,准备跳转', '检测中', examErrorCount, document.title);
setTimeout(autoJumpToLearningVideo, 1500);
observer.disconnect();
break;
}
}
}
if (m.type === 'attributes' && m.attributeName === 'style') {
const el = m.target;
if (el && /button|a/i.test(el.tagName)) {
const cs = getComputedStyle(el);
const bg = cs.backgroundColor || '';
if (/rgb\s*\(\s*25\d\s*,\s*10\d\s*,\s*10\d\s*\)/.test(bg) || /rgb\s*\(\s*253\s*,\s*103\s*,\s*103\s*\)/.test(bg)) {
updateStatusPanel('监控中', '状态更新,准备跳转', '检测中', examErrorCount, document.title);
setTimeout(autoJumpToLearningVideo, 1500);
observer.disconnect();
break;
}
}
}
}
});
observer.observe(targetNode, {
childList: true,
subtree: true,
characterData: true,
attributes: true
});
}
function clickFirstImmediateLearn() {
const btns = [
...document.querySelectorAll('input.state_lis_btn, input[type="button"], input[type="submit"]'),
...document.querySelectorAll('button, a[role="button"], a.el-button')
];
for (const btn of btns) {
const val = (btn.value || btn.innerText || '').trim().replace(/\s/g, '');
if (val.includes('立即学习') || val.includes('继续学习') || val.includes('去学习')) {
btn.click();
return;
}
}
}
function createContactPanel() {
if (document.getElementById('hy-contact-panel')) return;
const panel = document.createElement('div');
panel.id = 'hy-contact-panel';
panel.style.cssText = `
position: fixed; left: 10px; bottom: 10px; z-index: 999998;
background: rgba(255,255,255,0.92); color: #000;
padding: 10px; border-radius: 10px; font-size: 14px;
box-shadow: 0 0 8px rgba(0,0,0,0.2); text-align: center;
`;
panel.innerHTML = `
<div style="font-weight: bold; color: red; font-size: 16px;">如果没有时间可加v:Yyyyylaj</div>
<div style="display:flex; gap:8px; align-items:center; justify-content:center; margin-top:5px;">
<img src="https://i.ibb.co/d0fRsHkY/20250814140655-55-266.jpg" alt="打款码1" width="180" style="border-radius:10px;" />
<img src="https://i.ibb.co/FbmyTqhC/20250814140916-56-266.jpg" alt="打款码2" width="180" style="border-radius:10px;" />
</div>
<div style="margin-top:5px; font-size: 12px;">创作不易,感谢支持 ☕</div>
`;
document.body.appendChild(panel);
}
function init() {
const url = location.href;
createStatusPanel();
createContactPanel();
if (/course_ware\/course_ware_polyv\.aspx/i.test(url)) {
setTimeout(() => {
const originalOnPlayOver = window.s2j_onPlayOver;
window.s2j_onPlayOver = function () {
try {
if (typeof originalOnPlayOver === 'function') {
originalOnPlayOver.apply(this, arguments);
}
} catch (e) {}
setTimeout(() => autoJumpToLearningVideo(), 1000);
};
autoSkipPopup();
if (document.querySelector('video')) {
autoPlayVideo();
updateStatusPanel('运行中', '初始化完成', '检测中', examErrorCount, document.title);
} else {
updateStatusPanel('运行中', '未检测到视频,等待中', '检测中', examErrorCount, document.title);
}
observeStatusChange();
}, 1500);
}
if (/course_ware\/course_list\.aspx/i.test(url)) {
setTimeout(autoJumpToLearningVideo, 2000);
}
if (/\/exam_result\.aspx/i.test(url)) {
setTimeout(clickFirstImmediateLearn, 1500);
}
}
init();
})();