// ==UserScript==
// @name 华医网课程自动切换工具(增强版)
// @namespace https://github.com/user/auto-next-lesson-tool
// @version 2.0.0
// @description 自动检测华医网视频播放状态并在结束时切换到下一节课程,带悬浮控制面板
// @author Your Name
// @match *://*.91huayi.com/*
// @match *://yxpk.91huayi.com/*
// @match *://xuexi.91huayi.com/*
// @match *://*.huayiwang91.com/*
// @icon data:image/svg+xml,
// @grant none
// @license MIT
// @run-at document-end
// ==/UserScript==
/**
* 华医网课程自动切换工具(增强版)
* 版本: 2.0.0
*
* 本脚本专为华医网平台设计,可以自动检测视频播放状态,
* 在视频播放结束后自动切换到下一节课程。
*
* 支持的功能:
* - 自动检测华医网视频播放状态
* - 视频结束后自动点击"下一步"或"下一课"按钮
* - 自动处理视频播放过程中的弹窗提示
* - 支持自动静音播放
* - 提供详细的日志输出
* - 支持调试模式
* - 悬浮控制面板,方便实时控制
*/
/**
* 华医网课程自动切换工具类
* @class HuaYiAutoNextLesson
*/
class HuaYiAutoNextLesson {
/**
* 构造函数
* @param {Object} options - 配置选项
* @param {number} options.switchDelay - 切换延迟时间(秒),默认2秒
* @param {number} options.checkInterval - 检查间隔时间(秒),默认1秒
* @param {boolean} options.debugMode - 调试模式,默认false
* @param {boolean} options.autoMute - 自动静音,默认true
* @param {Function} options.onStateChange - 状态变化回调
* @param {Function} options.onVideoDetected - 视频检测回调
* @param {Function} options.onVideoEnded - 视频结束回调
* @param {Function} options.onLessonSwitched - 课程切换回调
* @param {Function} options.onError - 错误回调
*/
constructor(options = {}) {
this.options = {
switchDelay: 2, // 切换延迟时间(秒)
checkInterval: 1, // 检查间隔时间(秒)
debugMode: false, // 调试模式
autoMute: true, // 自动静音
showControlPanel: true, // 显示控制面板
panelPosition: 'top-right', // 面板位置:top-left, top-right, bottom-left, bottom-right
onStateChange: null, // 状态变化回调
onVideoDetected: null, // 视频检测回调
onVideoEnded: null, // 视频结束回调
onLessonSwitched: null, // 课程切换回调
onError: null // 错误回调
};
// 合并用户配置
Object.assign(this.options, options);
this.isRunning = false;
this.checkTimer = null;
this.switchTimer = null;
this.lastVideoState = null;
this.switchHistory = [];
this.currentCourseTitle = '';
this.currentLessonTitle = '';
this.controlPanel = null;
this.logContainer = null;
this.statusIndicator = null;
// 华医网特定的选择器
this.selectors = {
// 视频元素选择器
videoElements: [
'video',
'.video-js video',
'.vjs-tech',
'#video-player video',
'.video-container video',
'.player-container video',
'[id*="video"] video',
'[class*="video"] video'
],
// 下一课按钮选择器
nextLessonButtons: [
'.next-btn',
'.next-step',
'.next-lesson',
'.next-button',
'[id*="next"]',
'[class*="next"]',
'[onclick*="next"]',
'[href*="next"]',
'button:contains("下一步")',
'button:contains("下一课")',
'button:contains("继续学习")',
'a:contains("下一步")',
'a:contains("下一课")',
'a:contains("继续学习")',
'[id*="nextLesson"]',
'[class*="nextLesson"]',
'.btn-next',
'.step-next',
'.lesson-next'
],
// 弹窗关闭按钮选择器
popupCloseButtons: [
'.close-btn',
'.close',
'[data-dismiss="modal"]',
'.modal-close',
'.popup-close',
'[id*="close"]',
'[class*="close"]',
'button:contains("确定")',
'button:contains("知道了")',
'button:contains("关闭")',
'button:contains("继续")'
],
// 进度条选择器
progressBars: [
'.progress-bar',
'.progress',
'.vjs-progress-bar',
'.vjs-play-progress',
'[class*="progress"]',
'[id*="progress"]'
],
// 课程标题选择器
courseTitle: [
'.course-title',
'.course-name',
'h1',
'h2',
'[id*="course"]',
'[class*="course"]'
],
// 课时标题选择器
lessonTitle: [
'.lesson-title',
'.lesson-name',
'h3',
'h4',
'[id*="lesson"]',
'[class*="lesson"]'
]
};
this.log('华医网课程自动切换工具已初始化', 'info');
this._detectCurrentPage();
// 创建控制面板
if (this.options.showControlPanel) {
this._createControlPanel();
}
}
/**
* 检测当前页面类型
* @private
*/
_detectCurrentPage() {
const url = window.location.href.toLowerCase();
if (url.includes('/course/') || url.includes('/lesson/')) {
this.log('检测到课程播放页面', 'info');
this._extractPageInfo();
} else if (url.includes('/course/list') || url.includes('/courses')) {
this.log('检测到课程列表页面', 'info');
} else if (url.includes('/exam/') || url.includes('/test/')) {
this.log('检测到考试页面', 'info');
} else {
this.log('检测到其他页面', 'info');
}
}
/**
* 提取页面信息
* @private
*/
_extractPageInfo() {
// 提取课程标题
for (const selector of this.selectors.courseTitle) {
try {
const element = document.querySelector(selector);
if (element && element.textContent.trim()) {
this.currentCourseTitle = element.textContent.trim();
break;
}
} catch (error) {
this.log(`提取课程标题出错: ${error.message}`, 'debug');
}
}
// 提取课时标题
for (const selector of this.selectors.lessonTitle) {
try {
const element = document.querySelector(selector);
if (element && element.textContent.trim()) {
this.currentLessonTitle = element.textContent.trim();
break;
}
} catch (error) {
this.log(`提取课时标题出错: ${error.message}`, 'debug');
}
}
this.log(`当前课程: ${this.currentCourseTitle || '未知'}`, 'info');
this.log(`当前课时: ${this.currentLessonTitle || '未知'}`, 'info');
// 更新控制面板显示
this._updateControlPanelInfo();
}
/**
* 创建控制面板
* @private
*/
_createControlPanel() {
// 创建控制面板容器
this.controlPanel = document.createElement('div');
this.controlPanel.id = 'huayi-auto-next-control-panel';
this.controlPanel.className = 'huayi-control-panel';
// 设置面板样式
const panelStyles = `
position: fixed;
background: rgba(255, 255, 255, 0.95);
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 12px;
max-width: 320px;
z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 12px;
transition: all 0.3s ease;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
`;
// 根据位置设置样式
let positionStyles = '';
switch (this.options.panelPosition) {
case 'top-left':
positionStyles = 'top: 20px; left: 20px;';
break;
case 'bottom-left':
positionStyles = 'bottom: 20px; left: 20px;';
break;
case 'bottom-right':
positionStyles = 'bottom: 20px; right: 20px;';
break;
case 'top-right':
default:
positionStyles = 'top: 20px; right: 20px;';
break;
}
this.controlPanel.style.cssText = panelStyles + positionStyles;
// 创建面板内容
this.controlPanel.innerHTML = `
状态:
未启动
视频检测:
未检测
当前课程:
${this.currentCourseTitle || '未知'}
`;
// 添加到页面
document.body.appendChild(this.controlPanel);
// 绑定事件监听器
this._bindControlPanelEvents();
// 初始化状态
this._updateControlPanelState();
}
/**
* 绑定控制面板事件
* @private
*/
_bindControlPanelEvents() {
if (!this.controlPanel) return;
// 启动按钮
const startBtn = document.getElementById('huayi-btn-start');
if (startBtn) {
startBtn.addEventListener('click', () => {
this.start();
this._updateControlPanelState();
});
}
// 停止按钮
const stopBtn = document.getElementById('huayi-btn-stop');
if (stopBtn) {
stopBtn.addEventListener('click', () => {
this.stop();
this._updateControlPanelState();
});
}
// 静音按钮
const muteBtn = document.getElementById('huayi-btn-mute');
if (muteBtn) {
muteBtn.addEventListener('click', () => {
this.options.autoMute = !this.options.autoMute;
muteBtn.textContent = this.options.autoMute ? '静音: 开' : '静音: 关';
this.updateOptions({ autoMute: this.options.autoMute });
this.log(`${this.options.autoMute ? '开启' : '关闭'}自动静音`, 'info');
});
}
// 调试按钮
const debugBtn = document.getElementById('huayi-btn-debug');
if (debugBtn) {
debugBtn.addEventListener('click', () => {
this.options.debugMode = !this.options.debugMode;
debugBtn.textContent = this.options.debugMode ? '调试: 开' : '调试: 关';
this.updateOptions({ debugMode: this.options.debugMode });
this.log(`${this.options.debugMode ? '开启' : '关闭'}调试模式`, 'info');
});
}
// 切换延迟输入
const switchDelayInput = document.getElementById('huayi-switch-delay');
if (switchDelayInput) {
switchDelayInput.addEventListener('change', (e) => {
const value = parseFloat(e.target.value);
if (!isNaN(value) && value >= 0) {
this.updateOptions({ switchDelay: value });
this.log(`切换延迟已设置为 ${value} 秒`, 'info');
}
});
}
// 检查间隔输入
const checkIntervalInput = document.getElementById('huayi-check-interval');
if (checkIntervalInput) {
checkIntervalInput.addEventListener('change', (e) => {
const value = parseFloat(e.target.value);
if (!isNaN(value) && value > 0) {
this.updateOptions({ checkInterval: value });
this.log(`检查间隔已设置为 ${value} 秒`, 'info');
}
});
}
// 清空日志按钮
const clearLogsBtn = document.getElementById('huayi-btn-clear-logs');
if (clearLogsBtn) {
clearLogsBtn.addEventListener('click', () => {
this._clearLogs();
});
}
// 最小化按钮
const minimizeBtn = document.getElementById('huayi-btn-minimize');
if (minimizeBtn) {
minimizeBtn.addEventListener('click', () => {
this._togglePanelMinimize();
});
}
// 面板拖动功能
this._makePanelDraggable();
}
/**
* 使面板可拖动
* @private
*/
_makePanelDraggable() {
if (!this.controlPanel) return;
const header = this.controlPanel.querySelector('.panel-header');
if (!header) return;
let isDragging = false;
let offsetX, offsetY;
header.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'BUTTON' || e.target.closest('button')) {
return; // 避免点击按钮时触发拖动
}
isDragging = true;
offsetX = e.clientX - this.controlPanel.getBoundingClientRect().left;
offsetY = e.clientY - this.controlPanel.getBoundingClientRect().top;
this.controlPanel.style.cursor = 'grabbing';
header.style.userSelect = 'none';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const x = e.clientX - offsetX;
const y = e.clientY - offsetY;
// 限制在视口内
const maxX = window.innerWidth - this.controlPanel.offsetWidth;
const maxY = window.innerHeight - this.controlPanel.offsetHeight;
const boundedX = Math.max(0, Math.min(x, maxX));
const boundedY = Math.max(0, Math.min(y, maxY));
this.controlPanel.style.left = boundedX + 'px';
this.controlPanel.style.top = boundedY + 'px';
this.controlPanel.style.right = 'auto';
this.controlPanel.style.bottom = 'auto';
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
this.controlPanel.style.cursor = 'default';
header.style.userSelect = '';
}
});
}
/**
* 切换面板最小化状态
* @private
*/
_togglePanelMinimize() {
const content = document.getElementById('huayi-panel-content');
const minimizeBtn = document.getElementById('huayi-btn-minimize');
if (!content || !minimizeBtn) return;
if (content.style.display === 'none') {
content.style.display = 'block';
minimizeBtn.textContent = '▼';
} else {
content.style.display = 'none';
minimizeBtn.textContent = '▲';
}
}
/**
* 更新控制面板状态
* @private
*/
_updateControlPanelState() {
if (!this.controlPanel) return;
// 更新状态指示器
const statusIndicator = document.getElementById('huayi-status-indicator');
const statusValue = document.getElementById('huayi-status-value');
if (statusIndicator && statusValue) {
if (this.isRunning) {
statusIndicator.style.backgroundColor = '#10b981'; // 绿色
statusValue.textContent = '运行中';
statusValue.style.color = '#10b981';
} else {
statusIndicator.style.backgroundColor = '#6b7280'; // 灰色
statusValue.textContent = '已停止';
statusValue.style.color = '#6b7280';
}
}
// 更新按钮状态
const startBtn = document.getElementById('huayi-btn-start');
const stopBtn = document.getElementById('huayi-btn-stop');
if (startBtn && stopBtn) {
if (this.isRunning) {
startBtn.style.backgroundColor = '#9ca3af';
startBtn.disabled = true;
stopBtn.style.backgroundColor = '#ef4444';
stopBtn.disabled = false;
} else {
startBtn.style.backgroundColor = '#10b981';
startBtn.disabled = false;
stopBtn.style.backgroundColor = '#9ca3af';
stopBtn.disabled = true;
}
}
}
/**
* 更新控制面板信息
* @private
*/
_updateControlPanelInfo() {
if (!this.controlPanel) return;
// 更新课程标题
const courseTitleElement = document.getElementById('huayi-course-title');
if (courseTitleElement) {
courseTitleElement.textContent = this.currentCourseTitle || '未知';
}
}
/**
* 更新视频状态显示
* @param {Object} videoState - 视频状态
* @private
*/
_updateVideoStatusDisplay(videoState) {
if (!this.controlPanel) return;
const videoStatusElement = document.getElementById('huayi-video-status');
if (!videoStatusElement) return;
if (videoState.exists) {
const progressPercent = Math.round(videoState.progress * 100);
videoStatusElement.textContent = `播放中 ${progressPercent}%`;
videoStatusElement.style.color = videoState.isPaused ? '#f59e0b' : '#10b981';
} else {
videoStatusElement.textContent = '未检测';
videoStatusElement.style.color = '#9ca3af';
}
}
/**
* 添加日志到面板
* @param {string} message - 日志消息
* @param {string} level - 日志级别
* @private
*/
_addLogToPanel(message, level = 'info') {
if (!this.controlPanel) return;
const logsContainer = document.getElementById('huayi-logs-container');
if (!logsContainer) return;
const logEntry = document.createElement('div');
logEntry.className = `log-entry ${level}`;
logEntry.style.marginBottom = '2px';
logEntry.style.padding = '1px 0';
// 根据级别设置颜色
switch (level) {
case 'error':
logEntry.style.color = '#dc2626';
break;
case 'warning':
logEntry.style.color = '#d97706';
break;
case 'success':
logEntry.style.color = '#059669';
break;
case 'debug':
logEntry.style.color = '#6366f1';
break;
case 'info':
default:
logEntry.style.color = '#374151';
break;
}
// 限制消息长度
const shortMessage = message.length > 60 ? message.substring(0, 60) + '...' : message;
logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${shortMessage}`;
// 添加到容器开头
logsContainer.insertBefore(logEntry, logsContainer.firstChild);
// 限制日志数量
const logEntries = logsContainer.querySelectorAll('.log-entry');
if (logEntries.length > 10) {
logsContainer.removeChild(logEntries[logEntries.length - 1]);
}
// 滚动到底部
logsContainer.scrollTop = 0;
}
/**
* 清空日志
* @private
*/
_clearLogs() {
const logsContainer = document.getElementById('huayi-logs-container');
if (logsContainer) {
logsContainer.innerHTML = '日志已清空
';
}
}
/**
* 启动工具
* @returns {boolean} 是否启动成功
*/
start() {
if (this.isRunning) {
this.log('工具已经在运行', 'warning');
return false;
}
this.isRunning = true;
this.log('工具已启动', 'success');
this._notifyStateChange();
// 开始检测视频
this._startVideoCheck();
// 显示启动通知
this._showNotification('华医网课程自动切换工具已启动', '将自动检测视频并切换到下一课', 3000);
// 更新控制面板状态
this._updateControlPanelState();
return true;
}
/**
* 停止工具
* @returns {boolean} 是否停止成功
*/
stop() {
if (!this.isRunning) {
this.log('工具未运行', 'warning');
return false;
}
this.isRunning = false;
this._clearTimers();
this.log('工具已停止', 'info');
this._notifyStateChange();
// 更新控制面板状态
this._updateControlPanelState();
return true;
}
/**
* 更新配置
* @param {Object} newOptions - 新的配置选项
* @returns {boolean} 是否更新成功
*/
updateOptions(newOptions) {
Object.assign(this.options, newOptions);
this.log('配置已更新: ' + JSON.stringify(newOptions), 'info');
// 如果工具正在运行,重新启动检测
if (this.isRunning) {
this._clearTimers();
this._startVideoCheck();
}
return true;
}
/**
* 获取当前状态
* @returns {Object} 当前状态对象
*/
getState() {
return {
isRunning: this.isRunning,
options: { ...this.options },
switchHistory: [...this.switchHistory],
currentCourseTitle: this.currentCourseTitle,
currentLessonTitle: this.currentLessonTitle
};
}
/**
* 获取切换历史
* @returns {Array} 切换历史记录数组
*/
getSwitchHistory() {
return [...this.switchHistory];
}
/**
* 查找视频元素
* @returns {HTMLElement|null} 找到的视频元素或null
* @private
*/
_findVideoElement() {
// 尝试每个视频选择器
for (const selector of this.selectors.videoElements) {
try {
const elements = document.querySelectorAll(selector);
if (elements.length > 0) {
this.log(`使用选择器 "${selector}" 找到 ${elements.length} 个视频元素`, 'debug');
// 返回第一个可见的视频元素
for (const element of elements) {
if (this._isElementVisible(element)) {
return element;
}
}
}
} catch (error) {
this.log(`选择器 "${selector}" 执行出错: ${error.message}`, 'debug');
}
}
// 如果没有找到视频元素,尝试查找其他可能的视频播放元素
const videoContainers = document.querySelectorAll('.video-container, .player-container, .video-player');
if (videoContainers.length > 0) {
this.log(`找到 ${videoContainers.length} 个视频容器`, 'debug');
// 尝试在容器内查找视频元素
for (const container of videoContainers) {
const containerVideo = container.querySelector('video');
if (containerVideo && this._isElementVisible(containerVideo)) {
this.log('在视频容器内找到视频元素', 'debug');
return containerVideo;
}
}
}
return null;
}
/**
* 检查元素是否可见
* @param {HTMLElement} element - 要检查的元素
* @returns {boolean} 元素是否可见
* @private
*/
_isElementVisible(element) {
// 检查元素是否在DOM中
if (!document.body.contains(element)) {
return false;
}
// 检查元素的尺寸
const rect = element.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) {
return false;
}
// 检查元素是否被隐藏
const computedStyle = window.getComputedStyle(element);
if (computedStyle.display === 'none' ||
computedStyle.visibility === 'hidden' ||
computedStyle.opacity === '0') {
return false;
}
// 检查元素是否在视口中
const isInViewport = (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
return isInViewport;
}
/**
* 检查视频状态
* @param {HTMLElement} videoElement - 视频元素
* @returns {Object} 视频状态对象
* @private
*/
_checkVideoState(videoElement) {
try {
// 检查视频元素的基本属性
const currentTime = videoElement.currentTime || 0;
const duration = videoElement.duration || 0;
const progress = duration > 0 ? currentTime / duration : 0;
const isPaused = videoElement.paused;
const isEnded = videoElement.ended || progress >= 0.98; // 98%进度视为结束
const volume = videoElement.volume;
return {
exists: true,
currentTime: currentTime,
duration: duration,
progress: progress,
isPaused: isPaused,
isEnded: isEnded,
volume: volume
};
} catch (error) {
this.log(`检查视频状态出错: ${error.message}`, 'error');
return {
exists: false,
error: error.message
};
}
}
/**
* 开始视频检测
* @private
*/
_startVideoCheck() {
if (!this.isRunning) return;
this.log(`开始检测视频,检查间隔: ${this.options.checkInterval}秒`, 'debug');
const checkVideo = () => {
if (!this.isRunning) return;
try {
// 检查并关闭弹窗
this._closePopups();
// 查找视频元素
const videoElement = this._findVideoElement();
if (videoElement) {
this.log('检测到视频元素', 'debug');
// 如果启用了自动静音,检查并设置静音
if (this.options.autoMute && videoElement.volume > 0) {
videoElement.volume = 0;
this.log('已将视频设置为静音', 'info');
}
// 检查视频状态
const videoState = this._checkVideoState(videoElement);
// 更新视频状态显示
this._updateVideoStatusDisplay(videoState);
// 通知视频检测状态
if (this.options.onVideoDetected) {
this.options.onVideoDetected(videoState);
}
// 检查视频是否结束
if (videoState.isEnded && this.lastVideoState && !this.lastVideoState.isEnded) {
this.log('检测到视频播放结束', 'info');
// 通知视频结束
if (this.options.onVideoEnded) {
this.options.onVideoEnded(videoState);
}
// 延迟切换到下一课
this._scheduleSwitch();
}
this.lastVideoState = videoState;
} else {
this.log('未检测到视频元素', 'debug');
// 更新视频状态显示
this._updateVideoStatusDisplay({ exists: false });
// 通知未检测到视频
if (this.options.onVideoDetected) {
this.options.onVideoDetected({ exists: false });
}
this.lastVideoState = null;
}
} catch (error) {
this.log(`视频检测出错: ${error.message}`, 'error');
// 通知错误
if (this.options.onError) {
this.options.onError(error);
}
}
// 继续检测
this.checkTimer = setTimeout(checkVideo, this.options.checkInterval * 1000);
};
// 立即开始第一次检测
checkVideo();
}
/**
* 关闭弹窗
* @private
*/
_closePopups() {
for (const selector of this.selectors.popupCloseButtons) {
try {
// 处理:contains选择器
if (selector.includes(':contains(')) {
const text = selector.match(/:contains\("([^"]+)"\)/)[1];
const baseSelector = selector.replace(/:contains\("([^"]+)"\)/, '');
const elements = document.querySelectorAll(baseSelector);
for (const element of elements) {
if (element.textContent.includes(text) && this._isElementVisible(element)) {
element.click();
this.log(`点击了弹窗关闭按钮: ${text}`, 'info');
return;
}
}
} else {
// 处理普通选择器
const elements = document.querySelectorAll(selector);
for (const element of elements) {
if (this._isElementVisible(element)) {
element.click();
this.log(`点击了弹窗关闭按钮: ${selector}`, 'info');
return;
}
}
}
} catch (error) {
this.log(`关闭弹窗出错: ${error.message}`, 'debug');
}
}
}
/**
* 安排切换到下一课
* @private
*/
_scheduleSwitch() {
if (!this.isRunning) return;
this.log(`将在 ${this.options.switchDelay} 秒后切换到下一课`, 'info');
this.switchTimer = setTimeout(() => {
if (!this.isRunning) return;
try {
// 切换到下一课
this._switchToNextLesson();
} catch (error) {
this.log(`切换课程出错: ${error.message}`, 'error');
// 通知错误
if (this.options.onError) {
this.options.onError(error);
}
}
}, this.options.switchDelay * 1000);
}
/**
* 切换到下一课
* @private
*/
_switchToNextLesson() {
// 尝试各种选择器查找下一课按钮
for (const selector of this.selectors.nextLessonButtons) {
try {
// 处理:contains选择器
if (selector.includes(':contains(')) {
const text = selector.match(/:contains\("([^"]+)"\)/)[1];
const baseSelector = selector.replace(/:contains\("([^"]+)"\)/, '');
const elements = document.querySelectorAll(baseSelector);
for (const element of elements) {
if (element.textContent.includes(text) && this._isElementVisible(element)) {
this._clickNextButton(element, text);
return;
}
}
} else {
// 处理普通选择器
const elements = document.querySelectorAll(selector);
for (const element of elements) {
if (this._isElementVisible(element)) {
this._clickNextButton(element, selector);
return;
}
}
}
} catch (error) {
this.log(`查找下一课按钮出错: ${error.message}`, 'debug');
}
}
// 如果没有找到下一课按钮,尝试查找其他可能的导航元素
this.log('未找到标准的下一课按钮,尝试查找其他导航元素', 'warning');
// 尝试查找课程列表中的下一个课时
const lessonElements = document.querySelectorAll('.lesson-item, .course-item, [class*="lesson"], [class*="course"]');
if (lessonElements.length > 0) {
this.log(`找到 ${lessonElements.length} 个课时元素`, 'debug');
// 查找当前激活的课时
let currentIndex = -1;
for (let i = 0; i < lessonElements.length; i++) {
if (lessonElements[i].classList.contains('active') ||
lessonElements[i].classList.contains('current') ||
lessonElements[i].querySelector('.active') ||
lessonElements[i].querySelector('.current')) {
currentIndex = i;
break;
}
}
// 如果找到当前课时,尝试点击下一个课时
if (currentIndex >= 0 && currentIndex < lessonElements.length - 1) {
const nextLesson = lessonElements[currentIndex + 1];
const link = nextLesson.querySelector('a') || nextLesson;
if (link) {
this.log(`找到下一个课时,准备点击`, 'info');
this._clickNextButton(link, '课程列表中的下一课');
return;
}
}
}
this.log('未找到下一课按钮或链接', 'error');
// 通知错误
if (this.options.onError) {
this.options.onError(new Error('未找到下一课按钮或链接'));
}
}
/**
* 点击下一课按钮
* @param {HTMLElement} button - 下一课按钮元素
* @param {string} source - 按钮来源描述
* @private
*/
_clickNextButton(button, source) {
this.log(`准备点击下一课按钮: ${source}`, 'info');
try {
// 获取按钮文本作为下一课标题
let nextLessonTitle = '未知课程';
if (button.textContent.trim()) {
nextLessonTitle = button.textContent.trim();
} else if (button.getAttribute('aria-label')) {
nextLessonTitle = button.getAttribute('aria-label');
} else if (button.getAttribute('title')) {
nextLessonTitle = button.getAttribute('title');
}
// 模拟点击
const event = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window
});
button.dispatchEvent(event);
const timestamp = new Date().toLocaleTimeString();
// 记录切换历史
const switchRecord = {
fromCourse: this.currentCourseTitle,
fromLesson: this.currentLessonTitle,
toLesson: nextLessonTitle,
timestamp: timestamp,
source: source
};
this.switchHistory.unshift(switchRecord);
if (this.switchHistory.length > 10) {
this.switchHistory.pop(); // 保留最近10条记录
}
this.log(`已切换到下一课: ${nextLessonTitle}`, 'success');
// 更新当前课时标题
this.currentLessonTitle = nextLessonTitle;
// 显示切换通知
this._showNotification('课程已切换', `已从"${this.currentLessonTitle}"切换到"${nextLessonTitle}"`, 3000);
// 通知课程已切换
if (this.options.onLessonSwitched) {
this.options.onLessonSwitched(switchRecord);
}
} catch (error) {
this.log(`点击下一课按钮出错: ${error.message}`, 'error');
// 尝试直接点击
try {
button.click();
this.log('使用直接点击方法成功', 'info');
} catch (clickError) {
this.log(`直接点击也失败: ${clickError.message}`, 'error');
}
}
}
/**
* 清除所有定时器
* @private
*/
_clearTimers() {
if (this.checkTimer) {
clearTimeout(this.checkTimer);
this.checkTimer = null;
}
if (this.switchTimer) {
clearTimeout(this.switchTimer);
this.switchTimer = null;
}
}
/**
* 通知状态变化
* @private
*/
_notifyStateChange() {
if (this.options.onStateChange) {
this.options.onStateChange(this.getState());
}
}
/**
* 显示通知
* @param {string} title - 通知标题
* @param {string} message - 通知消息
* @param {number} duration - 显示时长(毫秒)
* @private
*/
_showNotification(title, message, duration = 3000) {
// 创建通知元素
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 80px;
right: 20px;
background: #fff;
border-left: 4px solid #3b82f6;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 12px 16px;
border-radius: 4px;
max-width: 300px;
z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
animation: slideIn 0.3s ease-out;
`;
// 添加样式
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
// 检查是否已经添加了样式
if (!document.getElementById('huayi-notification-style')) {
style.id = 'huayi-notification-style';
document.head.appendChild(style);
}
// 设置内容
notification.innerHTML = `
${title}
${message}
`;
// 添加到页面
document.body.appendChild(notification);
// 定时移除
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease-in';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}, duration);
}
/**
* 记录日志
* @param {string} message - 日志消息
* @param {string} level - 日志级别 (info, success, warning, error, debug)
* @private
*/
log(message, level = 'info') {
// 调试模式下显示所有日志,否则只显示非调试日志
if (level === 'debug' && !this.options.debugMode) {
return;
}
const timestamp = new Date().toLocaleTimeString();
const logEntry = {
message: message,
level: level,
timestamp: timestamp,
course: this.currentCourseTitle,
lesson: this.currentLessonTitle
};
// 输出到控制台
const consoleMethod = level === 'error' ? 'error' :
level === 'warning' ? 'warn' :
level === 'success' ? 'log' :
level === 'debug' ? 'debug' : 'info';
console[consoleMethod](`[HuaYiAutoNextLesson v2.0.0] [${timestamp}] ${message}`);
// 添加到面板日志
this._addLogToPanel(message, level);
// 触发自定义事件,以便UI可以监听并显示日志
if (typeof document !== 'undefined') {
try {
const event = new CustomEvent('huayi:log', { detail: logEntry });
document.dispatchEvent(event);
} catch (error) {
console.error(`[HuaYiAutoNextLesson] 触发日志事件失败: ${error.message}`);
}
}
}
}
// 创建全局实例
let huaYiAutoNextLessonInstance = null;
// 初始化函数
function initHuaYiAutoNextLesson() {
// 检查是否已经初始化
if (huaYiAutoNextLessonInstance) {
console.log('[HuaYiAutoNextLesson v2.0.0] 工具已经初始化');
return;
}
// 创建实例
huaYiAutoNextLessonInstance = new HuaYiAutoNextLesson({
switchDelay: 2,
checkInterval: 1,
debugMode: false,
autoMute: true,
showControlPanel: true,
panelPosition: 'top-right',
onStateChange: function(state) {
console.log(`[HuaYiAutoNextLesson v2.0.0] 状态变化: ${state.isRunning ? '运行中' : '已停止'}`);
},
onVideoDetected: function(videoState) {
if (videoState.exists) {
const progressPercent = Math.round(videoState.progress * 100);
console.log(`[HuaYiAutoNextLesson v2.0.0] 检测到视频,进度: ${progressPercent}%`);
} else {
console.log('[HuaYiAutoNextLesson v2.0.0] 未检测到视频');
}
},
onVideoEnded: function(videoState) {
console.log('[HuaYiAutoNextLesson v2.0.0] 视频已结束,准备切换到下一课');
},
onLessonSwitched: function(switchRecord) {
console.log(`[HuaYiAutoNextLesson v2.0.0] 已切换到: ${switchRecord.toLesson}`);
},
onError: function(error) {
console.error(`[HuaYiAutoNextLesson v2.0.0] 错误: ${error.message}`);
}
});
// 启动工具
huaYiAutoNextLessonInstance.start();
console.log('[HuaYiAutoNextLesson v2.0.0] 工具已初始化并启动');
console.log('[HuaYiAutoNextLesson v2.0.0] 提示: 控制面板已显示在页面右上角,可拖动调整位置');
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initHuaYiAutoNextLesson);
} else {
initHuaYiAutoNextLesson();
}
// 导出到全局作用域,方便在控制台中访问
window.HuaYiAutoNextLesson = HuaYiAutoNextLesson;
window.huaYiAutoNextLesson = huaYiAutoNextLessonInstance;
// 提供调试工具
window.HuaYiAutoNextLessonDebug = {
// 开启调试模式
enableDebug: function() {
if (huaYiAutoNextLessonInstance) {
huaYiAutoNextLessonInstance.updateOptions({ debugMode: true });
console.log('[HuaYiAutoNextLesson v2.0.0] 调试模式已开启');
}
},
// 关闭调试模式
disableDebug: function() {
if (huaYiAutoNextLessonInstance) {
huaYiAutoNextLessonInstance.updateOptions({ debugMode: false });
console.log('[HuaYiAutoNextLesson v2.0.0] 调试模式已关闭');
}
},
// 开启自动静音
enableAutoMute: function() {
if (huaYiAutoNextLessonInstance) {
huaYiAutoNextLessonInstance.updateOptions({ autoMute: true });
console.log('[HuaYiAutoNextLesson v2.0.0] 自动静音已开启');
}
},
// 关闭自动静音
disableAutoMute: function() {
if (huaYiAutoNextLessonInstance) {
huaYiAutoNextLessonInstance.updateOptions({ autoMute: false });
console.log('[HuaYiAutoNextLesson v2.0.0] 自动静音已关闭');
}
},
// 获取当前状态
getState: function() {
if (huaYiAutoNextLessonInstance) {
return huaYiAutoNextLessonInstance.getState();
}
return null;
},
// 获取切换历史
getHistory: function() {
if (huaYiAutoNextLessonInstance) {
return huaYiAutoNextLessonInstance.getSwitchHistory();
}
return [];
},
// 手动启动
start: function() {
if (huaYiAutoNextLessonInstance) {
huaYiAutoNextLessonInstance.start();
}
},
// 手动停止
stop: function() {
if (huaYiAutoNextLessonInstance) {
huaYiAutoNextLessonInstance.stop();
}
},
// 设置切换延迟
setSwitchDelay: function(seconds) {
if (huaYiAutoNextLessonInstance) {
huaYiAutoNextLessonInstance.updateOptions({ switchDelay: seconds });
console.log(`[HuaYiAutoNextLesson v2.0.0] 切换延迟已设置为 ${seconds} 秒`);
}
},
// 设置检查间隔
setCheckInterval: function(seconds) {
if (huaYiAutoNextLessonInstance) {
huaYiAutoNextLessonInstance.updateOptions({ checkInterval: seconds });
console.log(`[HuaYiAutoNextLesson v2.0.0] 检查间隔已设置为 ${seconds} 秒`);
}
},
// 切换面板位置
setPanelPosition: function(position) {
if (huaYiAutoNextLessonInstance) {
huaYiAutoNextLessonInstance.updateOptions({ panelPosition: position });
console.log(`[HuaYiAutoNextLesson v2.0.0] 面板位置已设置为: ${position}`);
}
}
};
// 为脚本猫添加版本信息
window.ScriptCat = window.ScriptCat || {};
window.ScriptCat.meta = window.ScriptCat.meta || {};
window.ScriptCat.meta.version = '2.0.0';
window.ScriptCat.meta.name = '华医网课程自动切换工具(增强版)';
window.ScriptCat.meta.description = '自动检测华医网视频播放状态并在结束时切换到下一节课程,带悬浮控制面板';
// 输出初始化信息
console.log('====================================================');
console.log('华医网课程自动切换工具 v2.0.0(增强版)');
console.log('专为华医网平台设计的自动课程切换工具');
console.log('====================================================');
console.log('支持的功能:');
console.log('- 自动检测视频播放状态');
console.log('- 视频结束后自动切换到下一课');
console.log('- 自动关闭弹窗提示');
console.log('- 自动静音播放');
console.log('- 悬浮控制面板,支持实时控制');
console.log('- 详细的日志输出');
console.log('');
console.log('调试命令:');
console.log('- HuaYiAutoNextLessonDebug.enableDebug() - 开启调试模式');
console.log('- HuaYiAutoNextLessonDebug.disableDebug() - 关闭调试模式');
console.log('- HuaYiAutoNextLessonDebug.enableAutoMute() - 开启自动静音');
console.log('- HuaYiAutoNextLessonDebug.disableAutoMute() - 关闭自动静音');
console.log('- HuaYiAutoNextLessonDebug.getState() - 获取当前状态');
console.log('- HuaYiAutoNextLessonDebug.getHistory() - 获取切换历史');
console.log('- HuaYiAutoNextLessonDebug.start() - 手动启动工具');
console.log('- HuaYiAutoNextLessonDebug.stop() - 手动停止工具');
console.log('- HuaYiAutoNextLessonDebug.setSwitchDelay(秒) - 设置切换延迟');
console.log('- HuaYiAutoNextLessonDebug.setCheckInterval(秒) - 设置检查间隔');
console.log('- HuaYiAutoNextLessonDebug.setPanelPosition(位置) - 设置面板位置');
console.log('面板位置选项: top-left, top-right, bottom-left, bottom-right');
console.log('====================================================');