// ==UserScript== // @name 六局云学堂学习助手 V1.0 // @namespace Violentmonkey Scripts // @match *://crsg.jianxianexuetang.cn/study/sub_cm/course/detail* // @match *://crsg.jianxianexuetang.cn/study/sub_cm/content/detail* // @grant none // @run-at document-start // @version 1.0 // @author 石港中心试验室出品 // @description (v1.0 Refactored) 仅保留自动播放和跳转模块 (脚本 A)。实现课程自动播放、PPT 自动浏览及跨章跳转,并包含强制从第一节课启动逻辑。 // ==/UserScript== (function() { "use strict"; // ========================================================================= // ## 常量配置 // ========================================================================= const CONFIG = { // 版本信息 VERSION: "v1.0", // 时间间隔配置 INTERVALS: { CHECK: 5000, REPORT: 5 * 60 * 1000, RETRY: 3000, VIDEO_WAIT: 5000, PPT_WAIT: 5000, MONITOR_DELAY: 10000, AUTO_JUMP_DELAY: 1500, AUTO_JUMP_TIMEOUT: 5000, STARTUP_DELAY: 1500, DETECT_DELAY: 1000, CONTENT_DETECT_DELAY: 3000, SCROLL_DELAY: 1000, TIME_EXPAND_DELAY: 1000, POPUP_CHECK_INTERVAL: 1000, POPUP_AUTO_CLOSE_DELAY: 500 }, // 选择器配置 SELECTORS: { VIDEO: 'video', PPT_SLIDES: '.img-item.document-item', PPT_CONTAINER: '.content-box.y-box', PPT_TIME_CONTAINER: '.gapDiv.font-btn-color.pd-l-5', PPT_TIME_ELEMENT: '.theme-color1', VIDEO_REMAINING: 'div.gapDiv > div > span', PHASE_LIST: '.phase-list .phase-item', CONTENT_LIST: '.content-list .content-item', ACTIVE_ITEM: '.content-item.active', FIRST_ITEM: '.phase-list .content-item', PANEL_HEADER: '#xuexi-header', PANEL_BODY: '#xuexi-body', // 弹窗相关选择器 POPUP_CONTAINER: '.el-dialog__wrapper, .ant-modal-root, .v-modal, .modal-mask, .popup-mask', POPUP_DIALOG: '.el-dialog, .ant-modal, .v-modal, .modal, .popup', POPUP_CONFIRM_BTN: '.el-button--primary, .ant-btn-primary, .confirm-btn, .ok-btn, .btn-primary', POPUP_CLOSE_BTN: '.el-dialog__headerbtn, .ant-modal-close, .close-btn, .modal-close', POPUP_OK_BTN: 'button:contains("确定"), button:contains("确认"), button:contains("OK"), button:contains("ok")', POPUP_YES_BTN: 'button:contains("是"), button:contains("Yes"), button:contains("yes")', POPUP_CONTENT: '.el-dialog__body, .ant-modal-body, .modal-body, .popup-content' }, // UI配置 UI: { PANEL_WIDTH: 260, PANEL_MIN_WIDTH: 120, PANEL_HEIGHT: 36, PANEL_POSITION: { top: '50px', left: '50px' }, BACKGROUND_URL: 'https://picsum.photos/seed/professional-blue/800/600.jpg' }, // 功能开关 FLAGS: { SKIP_VIDEOS_WITHOUT_REQUIREMENT: false, AUTO_HANDLE_POPUP: true, POPUP_DEBUG_MODE: false } }; // ========================================================================= // ## 工具类和辅助函数 // ========================================================================= class Logger { constructor(container) { this.container = container; this.logs = []; } log(message, type = 'info') { const timestamp = new Date().toLocaleTimeString(); const prefixes = { info: 'ℹ️', success: '✅', warn: '⚠️', error: '❌' }; const prefix = prefixes[type] || '•'; const logEntry = `[${timestamp}] ${prefix} ${message}`; this.logs.push(logEntry); if (this.container) { const line = document.createElement('div'); line.textContent = logEntry; this.container.appendChild(line); this.container.scrollTo({ top: this.container.scrollHeight, behavior: 'smooth' }); } console.log(`[六局云学堂 ${CONFIG.VERSION}] ${message}`); } info(message) { this.log(message, 'info'); } success(message) { this.log(message, 'success'); } warn(message) { this.log(message, 'warn'); } error(message) { this.log(message, 'error'); } copyToClipboard() { return navigator.clipboard.writeText(this.logs.join('\n')); } getLogs() { return this.logs.join('\n'); } } class TimeUtils { static formatSecondsToChinese(seconds) { seconds = Math.round(seconds); const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; return `(${minutes}分${remainingSeconds}秒)`; } static parseTimeToSeconds(timeText) { let totalSeconds = 0; const minuteMatch = timeText.match(/(\d+)分/); if (minuteMatch?.[1]) { totalSeconds += parseInt(minuteMatch[1], 10) * 60; } const secondMatch = timeText.match(/(\d+)秒/); if (secondMatch?.[1]) { totalSeconds += parseInt(secondMatch[1], 10); } return totalSeconds; } } class DOMUtils { static waitForElement(selector, timeout = 10000) { return new Promise((resolve) => { const element = document.querySelector(selector); if (element) { resolve(element); return; } const observer = new MutationObserver(() => { const element = document.querySelector(selector); if (element) { observer.disconnect(); resolve(element); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); resolve(null); }, timeout); }); } static async scrollToElement(element, options = {}) { if (!element) return; element.scrollIntoView({ behavior: 'smooth', block: 'center', ...options }); } static async slowScroll(container, targetScrollTop, duration) { return new Promise(resolve => { const startScrollTop = container.scrollTop; const distance = targetScrollTop - startScrollTop; let startTime = null; function easeOutQuad(t) { return t * (2 - t); } function animation(currentTime) { if (startTime === null) startTime = currentTime; const elapsedTime = currentTime - startTime; const progress = Math.min(elapsedTime / duration, 1); const easedProgress = easeOutQuad(progress); container.scrollTop = startScrollTop + distance * easedProgress; if (elapsedTime < duration) { requestAnimationFrame(animation); } else { container.scrollTop = targetScrollTop; resolve(); } } requestAnimationFrame(animation); }); } } // ========================================================================= // ## 弹窗监控器类 // ========================================================================= class PopupMonitor { constructor(logger) { this.logger = logger; this.monitoringTimer = null; this.handledPopups = new Set(); this.isMonitoring = false; } startMonitoring() { if (this.isMonitoring || !CONFIG.FLAGS.AUTO_HANDLE_POPUP) return; this.isMonitoring = true; this.logger.info('[弹窗] 开始监控弹窗...'); this.monitoringTimer = setInterval(() => { this.checkAndHandlePopups(); }, CONFIG.INTERVALS.POPUP_CHECK_INTERVAL); } stopMonitoring() { if (this.monitoringTimer) { clearInterval(this.monitoringTimer); this.monitoringTimer = null; } this.isMonitoring = false; this.handledPopups.clear(); this.logger.info('[弹窗] 停止监控弹窗'); } async checkAndHandlePopups() { try { // 检测各种类型的弹窗 const popups = [ ...this.findElementsBySelectors([ CONFIG.SELECTORS.POPUP_CONTAINER, CONFIG.SELECTORS.POPUP_DIALOG ]), ...this.findElementsByContent() ]; for (const popup of popups) { if (this.shouldHandlePopup(popup)) { await this.handlePopup(popup); } } } catch (error) { this.logger.error(`[弹窗] 检测过程中出错: ${error.message}`); } } findElementsBySelectors(selectors) { const elements = []; for (const selector of selectors) { try { const found = document.querySelectorAll(selector); elements.push(...Array.from(found)); } catch (error) { // 忽略无效选择器 } } return elements; } findElementsByContent() { const elements = []; // 查找包含特定文本的元素 const popupKeywords = ['确定', '确认', '提示', '警告', '通知', '消息', 'OK', 'ok', 'Yes', 'yes']; for (const keyword of popupKeywords) { const xpath = `//*[contains(text(), '${keyword}') and (self::div or self::p or self::span)]`; try { const result = document.evaluate(xpath, document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); for (let i = 0; i < result.snapshotLength; i++) { const element = result.snapshotItem(i); // 检查是否是弹窗内容的一部分 const parentDialog = element.closest('.el-dialog, .ant-modal, .v-modal, .modal, .popup'); if (parentDialog) { elements.push(parentDialog); } else if (window.getComputedStyle(element).position === 'fixed') { // 可能是独立弹窗 elements.push(element.parentElement || element); } } } catch (error) { // 忽略xpath错误 } } return elements; } shouldHandlePopup(popup) { if (!popup) return false; // 检查是否已经处理过 const popupId = this.getPopupId(popup); if (this.handledPopups.has(popupId)) return false; // 检查是否可见 if (!this.isPopupVisible(popup)) return false; return true; } getPopupId(popup) { // 生成弹窗的唯一标识 const id = popup.id || popup.className || popup.textContent?.substring(0, 50) || Math.random().toString(36); return `${id}_${popup.tagName}`; } isPopupVisible(popup) { try { const style = window.getComputedStyle(popup); const rect = popup.getBoundingClientRect(); return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && rect.width > 0 && rect.height > 0 && rect.top >= 0; } catch (error) { return false; } } async handlePopup(popup) { const popupId = this.getPopupId(popup); this.handledPopups.add(popupId); try { // 记录弹窗信息 const popupText = this.extractPopupText(popup); this.logger.info(`[弹窗] 检测到弹窗: "${popupText.substring(0, 50)}${popupText.length > 50 ? '...' : ''}"`); if (CONFIG.FLAGS.POPUP_DEBUG_MODE) { this.logger.info(`[弹窗] 调试模式 - 弹窗信息: ${popup.outerHTML.substring(0, 200)}...`); } // 尝试自动处理弹窗 const handled = await this.autoHandlePopup(popup); if (handled) { this.logger.success(`[弹窗] 成功处理弹窗`); } else { this.logger.warn(`[弹窗] 无法自动处理弹窗,将等待5秒后重试`); // 5秒后从处理列表中移除,以便重试 setTimeout(() => { this.handledPopups.delete(popupId); }, 5000); } } catch (error) { this.logger.error(`[弹窗] 处理弹窗时出错: ${error.message}`); this.handledPopups.delete(popupId); } } extractPopupText(popup) { const textElements = popup.querySelectorAll('p, div, span, h1, h2, h3, h4, h5, h6'); const texts = Array.from(textElements).map(el => el.textContent?.trim()).filter(text => text); return texts.join(' ').substring(0, 200); } async autoHandlePopup(popup) { // 策略1: 查找确认按钮 let confirmButton = this.findConfirmButton(popup); if (confirmButton) { await this.clickButton(confirmButton, '确认'); return true; } // 策略2: 查找OK按钮 let okButton = this.findOkButton(popup); if (okButton) { await this.clickButton(okButton, 'OK'); return true; } // 策略3: 查找是按钮 let yesButton = this.findYesButton(popup); if (yesButton) { await this.clickButton(yesButton, '是'); return true; } // 策略4: 查找主要按钮 let primaryButton = this.findPrimaryButton(popup); if (primaryButton) { await this.clickButton(primaryButton, '主要'); return true; } // 策略5: 查找关闭按钮 let closeButton = this.findCloseButton(popup); if (closeButton) { await this.clickButton(closeButton, '关闭'); return true; } // 策略6: 如果只有一个按钮,点击它 const allButtons = popup.querySelectorAll('button, .btn, [role="button"]'); if (allButtons.length === 1) { await this.clickButton(allButtons[0], '唯一'); return true; } return false; } findConfirmButton(popup) { const selectors = CONFIG.SELECTORS.POPUP_CONFIRM_BTN.split(', '); for (const selector of selectors) { try { const button = popup.querySelector(selector); if (button && this.isButtonVisible(button)) { return button; } } catch (error) { // 忽略选择器错误 } } // 查找包含确认文本的按钮 return this.findButtonByText(popup, ['确定', '确认', '确认']); } findOkButton(popup) { return this.findButtonByText(popup, ['OK', 'ok', '确定', '好的']); } findYesButton(popup) { return this.findButtonByText(popup, ['是', 'Yes', 'yes', '确定']); } findPrimaryButton(popup) { const selectors = [ '.el-button--primary', '.ant-btn-primary', '.btn-primary', '.primary-btn', '[type="submit"]' ]; for (const selector of selectors) { try { const button = popup.querySelector(selector); if (button && this.isButtonVisible(button)) { return button; } } catch (error) { // 忽略选择器错误 } } return null; } findCloseButton(popup) { const selectors = CONFIG.SELECTORS.POPUP_CLOSE_BTN.split(', '); for (const selector of selectors) { try { const button = popup.querySelector(selector); if (button && this.isButtonVisible(button)) { return button; } } catch (error) { // 忽略选择器错误 } } return null; } findButtonByText(popup, texts) { for (const text of texts) { // 使用CSS选择器查找包含文本的元素 const xpath = `//button[contains(text(), '${text}')] | //*[@role="button"][contains(text(), '${text}')] | //*[contains(@class, 'btn')][contains(text(), '${text}')]`; try { const result = document.evaluate(xpath, popup, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); const button = result.singleNodeValue; if (button && this.isButtonVisible(button)) { return button; } } catch (error) { // 忽略xpath错误 } } // 备用方法:遍历所有按钮 const allButtons = popup.querySelectorAll('button, .btn, [role="button"]'); for (const button of allButtons) { if (button.textContent && button.textContent.trim().includes(texts[0]) && this.isButtonVisible(button)) { return button; } } return null; } isButtonVisible(button) { try { const style = window.getComputedStyle(button); const rect = button.getBoundingClientRect(); return style.display !== 'none' && style.visibility !== 'hidden' && !button.disabled && rect.width > 0 && rect.height > 0; } catch (error) { return false; } } async clickButton(button, buttonType) { try { this.logger.info(`[弹窗] 点击${buttonType}按钮: "${button.textContent?.trim() || button.className}"`); // 滚动到按钮位置 button.scrollIntoView({ behavior: 'smooth', block: 'center' }); // 等待一小段时间 await this.delay(CONFIG.INTERVALS.POPUP_AUTO_CLOSE_DELAY); // 创建鼠标事件 const clickEvent = new MouseEvent('click', { view: window, bubbles: true, cancelable: true, clientX: button.getBoundingClientRect().left + 5, clientY: button.getBoundingClientRect().top + 5 }); // 触发点击事件 button.dispatchEvent(clickEvent); // 备用方法:直接调用click方法 if (typeof button.click === 'function') { button.click(); } return true; } catch (error) { this.logger.error(`[弹窗] 点击${buttonType}按钮失败: ${error.message}`); return false; } } delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // 手动处理弹窗的方法 async handleCurrentPopup() { const popups = [ ...this.findElementsBySelectors([ CONFIG.SELECTORS.POPUP_CONTAINER, CONFIG.SELECTORS.POPUP_DIALOG ]), ...this.findElementsByContent() ]; if (popups.length === 0) { this.logger.info('[弹窗] 当前没有检测到弹窗'); return false; } for (const popup of popups) { if (this.shouldHandlePopup(popup)) { await this.handlePopup(popup); return true; } } return false; } } // ========================================================================= // ## UI管理类 // ========================================================================= class UIPanel { constructor() { this.panel = null; this.header = null; this.body = null; this.logger = null; this.isMinimized = false; this.isDragging = false; this.dragOffset = { x: 0, y: 0 }; } init() { this.createPanel(); this.setupStyles(); this.setupDragFunctionality(); this.setupMinimizeToggle(); this.setupCopyButton(); this.logger = new Logger(this.body); this.logger.info('[系统] 六局云学堂已启动,等待内容加载...'); return this.logger; } createPanel() { this.panel = document.createElement('div'); this.panel.innerHTML = `
六局云学堂 ${CONFIG.VERSION}
`; Object.assign(this.panel.style, { position: 'fixed', top: CONFIG.UI.PANEL_POSITION.top, left: CONFIG.UI.PANEL_POSITION.left, width: `${CONFIG.UI.PANEL_WIDTH}px`, background: 'rgba(30,30,30,0.35)', backgroundImage: `url("${CONFIG.UI.BACKGROUND_URL}")`, backgroundSize: 'cover', backgroundPosition: 'center', backgroundBlendMode: 'overlay', border: '1px solid rgba(255,255,255,0.25)', borderRadius: '10px', backdropFilter: 'blur(12px)', color: '#fff', zIndex: 9999999, userSelect: 'none', boxShadow: '0 4px 20px rgba(0,0,0,0.3)', overflow: 'hidden' }); document.body.appendChild(this.panel); this.header = this.panel.querySelector(CONFIG.SELECTORS.PANEL_HEADER); this.body = this.panel.querySelector(CONFIG.SELECTORS.PANEL_BODY); } setupStyles() { const style = document.createElement('style'); style.textContent = ` #xuexi-body::-webkit-scrollbar { width: 6px; } #xuexi-body::-webkit-scrollbar-track { background: rgba(255,255,255,0.1); border-radius:10px; } #xuexi-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.3); border-radius:10px; } #xuexi-body::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.5); } `; document.head.appendChild(style); } setupDragFunctionality() { this.header.addEventListener('mousedown', (e) => { this.isDragging = true; this.dragOffset.x = e.clientX - this.panel.offsetLeft; this.dragOffset.y = e.clientY - this.panel.offsetTop; const handleMouseMove = (e) => { if (!this.isDragging) return; this.panel.style.left = `${e.clientX - this.dragOffset.x}px`; this.panel.style.top = `${e.clientY - this.dragOffset.y}px`; }; const handleMouseUp = () => { this.isDragging = false; document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }); } setupMinimizeToggle() { this.header.addEventListener('dblclick', () => { this.isMinimized = !this.isMinimized; if (this.isMinimized) { this.body.style.display = 'none'; this.panel.style.width = `${CONFIG.UI.PANEL_MIN_WIDTH}px`; this.panel.style.height = `${CONFIG.UI.PANEL_HEIGHT}px`; this.panel.style.boxShadow = '0 2px 6px rgba(0,0,0,0.15)'; this.header.style.fontSize = '12px'; this.header.style.padding = '4px 0'; this.header.textContent = '六局云学堂(最小化)'; this.copyBtn.style.display = 'none'; this.popupBtn.style.display = 'none'; } else { this.body.style.display = 'block'; this.panel.style.width = `${CONFIG.UI.PANEL_WIDTH}px`; this.panel.style.height = 'auto'; this.panel.style.boxShadow = '0 4px 20px rgba(0,0,0,0.3)'; this.header.style.fontSize = '14px'; this.header.style.padding = '6px 0'; this.header.textContent = `学六局云学堂 ${CONFIG.VERSION}`; this.copyBtn.style.display = 'flex'; this.popupBtn.style.display = 'flex'; } }); } setupControlButtons() { // 复制日志按钮 this.copyBtn = document.createElement('div'); this.copyBtn.textContent = '📋'; Object.assign(this.copyBtn.style, { position: 'absolute', top: '6px', right: '8px', cursor: 'pointer', color: '#fff', fontWeight: 'bold', userSelect: 'none', fontSize: '12px', width: '16px', height: '16px', display: 'flex', alignItems: 'center', justifyContent: 'center' }); this.copyBtn.title = '点击复制日志'; this.panel.appendChild(this.copyBtn); this.copyBtn.addEventListener('click', async () => { try { await this.logger.copyToClipboard(); this.logger.success('日志已复制到剪贴板'); } catch (error) { this.logger.error('复制失败,请手动复制'); } }); // 弹窗处理按钮 this.popupBtn = document.createElement('div'); this.popupBtn.textContent = '🎯'; Object.assign(this.popupBtn.style, { position: 'absolute', top: '6px', right: '30px', cursor: 'pointer', color: '#fff', fontWeight: 'bold', userSelect: 'none', fontSize: '12px', width: '16px', height: '16px', display: 'flex', alignItems: 'center', justifyContent: 'center' }); this.popupBtn.title = '手动处理弹窗'; this.panel.appendChild(this.popupBtn); // 弹窗按钮点击事件将在外部设置 } setupCopyButton() { // 重定向到新的控制按钮设置方法 this.setupControlButtons(); } setPopupButtonCallback(callback) { if (this.popupBtn && callback) { this.popupBtn.addEventListener('click', callback); } } } // ========================================================================= // ## 内容检测器类 // ========================================================================= class ContentDetector { constructor(logger) { this.logger = logger; } checkPptElements() { const slides = document.querySelectorAll(CONFIG.SELECTORS.PPT_SLIDES); if (slides.length === 0) return false; const scrollContainer = document.querySelector(CONFIG.SELECTORS.PPT_CONTAINER); const firstSlideImg = slides[0].querySelector('img'); return !!(scrollContainer && firstSlideImg); } detectContentType() { if (this.checkPptElements()) { return 'ppt'; } else if (document.querySelector(CONFIG.SELECTORS.VIDEO)) { return 'video'; } return 'unknown'; } getVideoElement() { return document.querySelector(CONFIG.SELECTORS.VIDEO); } getPptElements() { return { slides: document.querySelectorAll(CONFIG.SELECTORS.PPT_SLIDES), container: document.querySelector(CONFIG.SELECTORS.PPT_CONTAINER), timeContainer: document.querySelector(CONFIG.SELECTORS.PPT_TIME_CONTAINER), timeElement: document.querySelector(CONFIG.SELECTORS.PPT_TIME_ELEMENT) }; } getVideoRemainingElement() { return document.querySelector(CONFIG.SELECTORS.VIDEO_REMAINING); } getCurrentActiveItem() { return document.querySelector(CONFIG.SELECTORS.ACTIVE_ITEM); } getFirstItem() { return document.querySelector(CONFIG.SELECTORS.FIRST_ITEM); } getCourseStructure() { const phaseList = [...document.querySelectorAll(CONFIG.SELECTORS.PHASE_LIST)]; const currentActive = this.getCurrentActiveItem(); if (!currentActive) return null; const currentPhase = currentActive.closest('.phase-item'); const contentList = [...currentPhase.querySelectorAll(CONFIG.SELECTORS.CONTENT_LIST)]; const currentIndex = contentList.indexOf(currentActive); const phaseIndex = phaseList.indexOf(currentPhase); return { phaseList, currentPhase, contentList, currentIndex, phaseIndex, currentActive }; } } // ========================================================================= // ## 视频处理器类 // ========================================================================= class VideoHandler { constructor(logger, detector) { this.logger = logger; this.detector = detector; this.lastVideoSrc = null; this.progressTimer = null; this.lastProgressReport = 0; } async handlePlayback() { const video = this.detector.getVideoElement(); if (!video) { this.logger.error('[视频] 启动失败,