// ==UserScript== // @name 宝武学习助手 // @namespace http://tampermonkey.net/ // @version 1.3.8 // @description 1.自动静音、自动播放、自动下一节;2.自动完成学习时长;3.学习进度状态显示 // @author jinxhcn // @match *://learn.baowugroup.com/* // @icon https://learn.baowugroup.com/favicon.png // @grant GM_info // @grant unsafeWindow // @license MIT // @run-at document-idle // ==/UserScript== (function () { 'use strict'; console.log(`✅ ${GM_info.script.name} v${GM_info.script.version}`); const getTime = () => { const d = new Date(); return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`; }; const sleep = ms => new Promise(r => setTimeout(r, ms)); const isVisible = el => { if (!el) return false; try { return !!(el.offsetParent || el.getClientRects().length > 0); } catch { return false; } }; const CONFIG = { CHECK_INTERVAL: 2000, DIALOG_WAIT: 1000, DATA_LOAD_WAIT: 3000, STATUS_BAR_ID: 'bw-learn-status-bar', STATUS_STYLES_ID: 'bw-learn-status-styles', MAX_LOG_LINES: 500, PROGRESS_FETCH_INTERVAL: 180000 }; const S = { hasClickedNext: false, lastVideoSrc: '', waitingFirstPlay: false, timer: null, uiFixed: false, courseCompleted: false, completionChecked: false, playPending: false, unlockTimer: null, isFetchingProgress: false, statusBar: null, logContainer: null, videoExists: false, isCollapsed: false, progressEnabled: true, progressTimer: null, checkIntervalId: null }; const findVisibleButton = (...texts) => { const btns = document.querySelectorAll('button'); for (const btn of btns) { if (isVisible(btn) && texts.some(t => btn.innerText.trim().includes(t))) return btn; } return null; }; const clickButtonByText = (...texts) => { const btn = findVisibleButton(...texts); if (btn) { btn.click(); return true; } return false; }; const waitForDialog = async (title, timeout = 2000) => { const start = Date.now(); while (Date.now() - start < timeout) { const dialogs = document.querySelectorAll('.el-dialog,[role="dialog"]'); for (const d of dialogs) { const h = d.querySelector('.el-dialog__header .text'); const label = d.getAttribute('aria-label'); if ((h && h.textContent.trim() === title) || label === title) return d; } await sleep(200); } return null; }; const clearUnlock = () => { if (S.unlockTimer) { clearTimeout(S.unlockTimer); S.unlockTimer = null; } }; const getVideo = () => document.querySelector('#xg-video video'); const ensureMuted = v => { if (v && !v.muted) { v.muted = true; console.log('🔇 已静音'); } }; const hasNextSection = () => { const cur = document.querySelector('.el-tree-node.is-current'); if (!cur) return false; let node = cur; while (node) { let next = node.nextElementSibling; while (next) { if (next.classList.contains('el-tree-node')) return true; next = next.nextElementSibling; } node = node.parentElement?.closest?.('.el-tree-node'); } return false; }; const clickNextSection = () => { if (clickButtonByText('播放下一节', '下一节')) { console.log('✅ 已点击"下一节"'); return true; } const hints = document.querySelectorAll('*'); for (const el of hints) { if (isVisible(el) && el.innerText?.trim() === '本节播放结束, 是否继续播放?') { const container = el.closest('.play-chapter') || el.parentElement; const btn = container?.querySelector('button'); if (btn && isVisible(btn)) { btn.click(); console.log('✅ 已点击"下一节"(通过结束提示)'); return true; } break; } } const primaryBtns = document.querySelectorAll('.el-button--primary'); for (const btn of primaryBtns) { if (isVisible(btn) && btn.innerText.includes('下一节')) { btn.click(); console.log('✅ 已点击"下一节"(Primary 按钮)'); return true; } } const cur = document.querySelector('.el-tree-node.is-current'); if (cur) { const nextSibling = cur.nextElementSibling; if (nextSibling && nextSibling.classList.contains('el-tree-node')) { const label = nextSibling.querySelector('.el-tree-node__label, .course-name'); if (label) { label.click(); console.log('✅ 已点击下一节(通过课程目录树)'); return true; } } } console.warn('⚠️ 未找到"下一节"按钮'); return false; }; const clickReplayButton = () => { const containers = document.querySelectorAll('.play-chapter'); for (const c of containers) { if (isVisible(c)) { const btn = c.querySelector('button'); if (btn && isVisible(btn) && btn.innerText.includes('重新播放')) { btn.click(); console.log('🔄 已点击"重新播放"按钮(时长未达标)'); return true; } } } if (clickButtonByText('重新播放')) { console.log('🔄 已点击"重新播放"按钮(备用查找)'); return true; } console.warn('⚠️ 未找到"重新播放"按钮'); return false; }; const playCurrentVideo = (video) => { if (!video || video.ended || !video.paused) return false; ensureMuted(video); const strategies = [ { sel: '.xgplayer-start', name: '中央播放按钮' }, { sel: '.xgplayer-play .xgplayer-icon', name: '控制栏播放图标' }, { sel: '#xg-video, .xgplayer', name: '视频画面区域' }, ]; for (const { sel, name } of strategies) { const el = document.querySelector(sel); if (el?.offsetParent) { el.click(); console.log(`▶️ 点击${name}`); return true; } } video.play().catch(() => { }); console.log('▶️ 调用原生 video.play()'); return true; }; const resetPlayState = () => { clearTimeout(S.timer); clearUnlock(); S.playPending = false; S.hasClickedNext = false; S.waitingFirstPlay = true; S.uiFixed = false; S.timer = setTimeout(() => { S.waitingFirstPlay = false; if (!document.hidden) { const v = getVideo(); if (v && !S.playPending) { ensureMuted(v); S.playPending = true; playCurrentVideo(v); S.unlockTimer = setTimeout(() => { S.playPending = false; S.unlockTimer = null; }, 1500); } } }, 300); }; const extractProgressFromDialog = (dialog) => { const rows = dialog.querySelectorAll('tr.vxe-body--row'); let total = 0, learned = 0, passScore = 0, courseScore = 0; for (const row of rows) { const cells = row.querySelectorAll('td.vxe-body--column'); if (cells.length < 3) continue; const t1 = cells[0].textContent.trim(); if (t1.includes('学习时长')) { let m = t1.match(/[((]\s*(\d+\.?\d*)\s*分钟[))]/) || t1.match(/(\d+\.?\d*)\s*分钟/); if (m) total = parseFloat(m[1]); const t3 = cells[2].textContent.trim().match(/(\d+\.?\d*)\s*分钟/); if (t3) learned = parseFloat(t3[1]); break; } } const scoreItems = dialog.querySelectorAll('.score-info .score-item'); for (const item of scoreItems) { const label = item.querySelector('.label'); const value = item.querySelector('.value'); if (label && value && label.textContent.trim().includes('合格分数线')) { const m = value.textContent.trim().match(/(\d+\.?\d*)/); if (m) passScore = parseFloat(m[1]); break; } } const statItems = dialog.querySelectorAll('.table-footer .stat-item'); for (const item of statItems) { const label = item.querySelector('.label'); const value = item.querySelector('.value'); if (label && value && label.textContent.trim().includes('课程成绩')) { const m = value.textContent.trim().match(/(\d+\.?\d*)/); if (m) courseScore = parseFloat(m[1]); break; } } return { total, learned, passScore, courseScore }; }; const closeProgressDialog = (dialog) => { const btn = dialog.querySelector('.el-dialog__headerbtn'); if (btn) { btn.click(); return; } const overlay = dialog.closest('.el-overlay'); if (overlay) { const mask = overlay.querySelector('.el-overlay-dialog'); mask?.click(); } }; const fetchProgress = async (quiet = false) => { if (S.isFetchingProgress) return null; S.isFetchingProgress = true; try { const btn = findVisibleButton('完成情况'); if (!btn) { if (!quiet) addStatusLog('📊 未找到"完成情况"按钮', 'warn'); return null; } btn.click(); console.log('📊 正在获取学习进度...'); const dialog = await waitForDialog('完成情况', 2000); if (!dialog) { if (!quiet) addStatusLog('📊 等待进度弹窗超时', 'warn'); return null; } await sleep(3000); let data = extractProgressFromDialog(dialog); if (data.total === 0) { await sleep(2000); data = extractProgressFromDialog(dialog); if (data.total === 0) { await sleep(2000); data = extractProgressFromDialog(dialog); } } closeProgressDialog(dialog); if (data.total > 0) { updateProgressDisplay(data); console.log(`📊 进度:${data.learned}/${data.total}分钟`); } return data; } finally { S.isFetchingProgress = false; } }; const checkAndReplayIfNeeded = async () => { if (S.isFetchingProgress) return; const data = await fetchProgress(true); if (!data || data.total === 0) { S.completionChecked = false; return; } if (data.learned >= data.total) { console.log(`✅ 学习时长已达标(${data.learned}/${data.total}),停止自动操作`); S.courseCompleted = true; onCourseCompleted(); return; } const video = getVideo(); if (!video) return; if (video.ended && !hasNextSection()) { console.log(`⏰ 学习时长不足(${data.learned}/${data.total}),尝试重新播放`); if (clickReplayButton()) { resetPlayState(); S.completionChecked = false; S.courseCompleted = false; } else { S.completionChecked = false; } } }; const injectStyles = () => { if (document.getElementById(CONFIG.STATUS_STYLES_ID)) return; const style = document.createElement('style'); style.id = CONFIG.STATUS_STYLES_ID; style.textContent = ` #${CONFIG.STATUS_BAR_ID} { position: fixed; bottom: 20px; right: 16px; width: 360px; max-height: 320px; background: rgba(20,20,30,0.92); color: #e0e0e0; border-radius: 10px; padding: 12px 14px; z-index: 99999; font-family: 'Microsoft YaHei','PingFang SC',Arial,sans-serif; font-size: 13px; box-shadow: 0 6px 28px rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.08); display: none; overflow: hidden; backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); transition: all 0.35s cubic-bezier(0.4,0,0.2,1); } #${CONFIG.STATUS_BAR_ID}.visible { display: block; } #${CONFIG.STATUS_BAR_ID}.collapsed { width: auto; min-width: 30px; height: 30px; border-radius: 15px; padding: 0; cursor: pointer; overflow: hidden; background: rgba(20,20,30,0.88); border: 1px solid rgba(79,195,247,0.3); box-shadow: 0 2px 16px rgba(79,195,247,0.15); display: flex !important; align-items: center; justify-content: center; right: 14px; bottom: 14px; backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); transition: all 0.3s cubic-bezier(0.4,0,0.2,1); } #${CONFIG.STATUS_BAR_ID}.collapsed:hover { border-color: rgba(79,195,247,0.6); box-shadow: 0 4px 24px rgba(79,195,247,0.25); transform: scale(1.05); } #${CONFIG.STATUS_BAR_ID}.collapsed .sb-header { border:none; margin:0; padding:0; width:auto; gap:0; white-space:nowrap; justify-content:center; } #${CONFIG.STATUS_BAR_ID}.collapsed .sb-header .sb-badge { display:none; } #${CONFIG.STATUS_BAR_ID}.collapsed .sb-header .sb-title { font-size:0; color:transparent; gap:0; } #${CONFIG.STATUS_BAR_ID}.collapsed .sb-header .sb-title .sb-video-dot { display:inline-block !important; width:8px; height:8px; margin:0; animation:bw-pulse 2s ease-in-out infinite; } @keyframes bw-pulse { 0%,100%{opacity:1;transform:scale(1);} 50%{opacity:0.5;transform:scale(0.85);} } #${CONFIG.STATUS_BAR_ID}.collapsed .sb-progress-row, #${CONFIG.STATUS_BAR_ID}.collapsed .sb-score-row, #${CONFIG.STATUS_BAR_ID}.collapsed .sb-log-box, #${CONFIG.STATUS_BAR_ID}.collapsed .sb-footer, #${CONFIG.STATUS_BAR_ID}.collapsed .sb-version, #${CONFIG.STATUS_BAR_ID}.collapsed .sb-badge { display:none !important; } #${CONFIG.STATUS_BAR_ID}.collapsed .sb-collapse-btn { display:none !important; } #${CONFIG.STATUS_BAR_ID} .sb-collapse-btn { cursor:pointer; background:none; border:none; color:rgba(255,255,255,0.35); font-size:16px; padding:0 2px; transition:color 0.2s; line-height:1; margin-left:4px; } #${CONFIG.STATUS_BAR_ID} .sb-collapse-btn:hover { color:rgba(255,255,255,0.7); } #${CONFIG.STATUS_BAR_ID} .sb-header { display:flex; align-items:center; justify-content:space-between; padding-bottom:6px; border-bottom:1px solid rgba(255,255,255,0.1); margin-bottom:4px; } #${CONFIG.STATUS_BAR_ID} .sb-title { font-weight:600; font-size:14px; color:#4fc3f7; display:flex; align-items:center; gap:6px; } #${CONFIG.STATUS_BAR_ID} .sb-version { font-size:10px; color:rgba(255,255,255,0.3); font-weight:400; margin-left:-2px; } #${CONFIG.STATUS_BAR_ID} .sb-badge { padding:2px 12px; border-radius:14px; font-size:12px; font-weight:600; transition:all 0.3s ease; } #${CONFIG.STATUS_BAR_ID} .sb-badge.learning { background:#e65100; color:#ffcc80; } #${CONFIG.STATUS_BAR_ID} .sb-badge.completed { background:#2e7d32; color:#a5d6a7; } #${CONFIG.STATUS_BAR_ID} .sb-badge.waiting { background:#1565c0; color:#90caf9; } #${CONFIG.STATUS_BAR_ID} .sb-badge.paused { background:#37474f; color:#90a4ae; } #${CONFIG.STATUS_BAR_ID} .sb-progress-row { display:flex; align-items:center; gap:8px; padding:8px 4px 10px 6px; margin-bottom:6px; font-size:12px; background:rgba(255,255,255,0.03); border-radius:6px; } #${CONFIG.STATUS_BAR_ID} .sb-progress-info { color:#b0bec5; white-space:nowrap; min-width:70px; font-size:12px; } #${CONFIG.STATUS_BAR_ID} .sb-progress-bar-wrap { flex:1; height:8px; background:rgba(255,255,255,0.08); border-radius:4px; overflow:hidden; } #${CONFIG.STATUS_BAR_ID} .sb-progress-bar { height:100%; width:0%; background:linear-gradient(90deg,#4fc3f7,#29b6f6); border-radius:4px; transition:width 0.6s cubic-bezier(0.4,0,0.2,1); } #${CONFIG.STATUS_BAR_ID} .sb-toggle-wrap { display:flex; align-items:center; gap:4px; cursor:pointer; user-select:none; flex-shrink:0; } #${CONFIG.STATUS_BAR_ID} .sb-toggle-track { position:relative; width:32px; height:18px; background:rgba(255,255,255,0.15); border-radius:9px; transition:background 0.25s ease; } #${CONFIG.STATUS_BAR_ID} .sb-toggle-track.on { background:#4fc3f7; } #${CONFIG.STATUS_BAR_ID} .sb-toggle-thumb { position:absolute; top:2px; left:2px; width:14px; height:14px; background:#fff; border-radius:50%; transition:transform 0.25s cubic-bezier(0.4,0,0.2,1); box-shadow:0 1px 3px rgba(0,0,0,0.3); } #${CONFIG.STATUS_BAR_ID} .sb-toggle-track.on .sb-toggle-thumb { transform:translateX(14px); } #${CONFIG.STATUS_BAR_ID} .sb-score-row { display:flex; align-items:center; gap:12px; padding:6px 6px 8px 6px; margin-bottom:6px; font-size:12px; background:rgba(255,255,255,0.03); border-radius:6px; } #${CONFIG.STATUS_BAR_ID} .sb-score-item { color:#b0bec5; white-space:nowrap; font-size:12px; } #${CONFIG.STATUS_BAR_ID} .sb-score-item .sb-score-label { color:rgba(255,255,255,0.4); } #${CONFIG.STATUS_BAR_ID} .sb-score-item .sb-score-value { color:#4fc3f7; font-weight:600; } #${CONFIG.STATUS_BAR_ID} .sb-score-item .sb-score-pass { color:#ffb74d; font-weight:600; } #${CONFIG.STATUS_BAR_ID} .sb-score-item .sb-score-course { color:#81c784; font-weight:600; } #${CONFIG.STATUS_BAR_ID} .sb-log-box { max-height:170px; border:1px solid rgba(255,255,255,0.04); border-radius:6px; background:rgba(0,0,0,0.15); padding:4px 6px; overflow-y:auto; font-size:12px; line-height:1.7; } #${CONFIG.STATUS_BAR_ID} .sb-log-box::-webkit-scrollbar { width:4px; } #${CONFIG.STATUS_BAR_ID} .sb-log-box::-webkit-scrollbar-thumb { background:rgba(255,255,255,0.2); border-radius:4px; } #${CONFIG.STATUS_BAR_ID} .sb-log-box::-webkit-scrollbar-track { background:transparent; } #${CONFIG.STATUS_BAR_ID} .sb-log-entry { padding:2px 4px; border-bottom:1px solid rgba(255,255,255,0.03); word-break:break-all; font-family:'Consolas','Courier New',monospace; font-size:11.5px; } #${CONFIG.STATUS_BAR_ID} .sb-log-entry.info { color:#b0bec5; } #${CONFIG.STATUS_BAR_ID} .sb-log-entry.success { color:#81c784; } #${CONFIG.STATUS_BAR_ID} .sb-log-entry.warn { color:#ffb74d; } #${CONFIG.STATUS_BAR_ID} .sb-log-entry.error { color:#ef5350; } #${CONFIG.STATUS_BAR_ID} .sb-log-entry.action { color:#64b5f6; } #${CONFIG.STATUS_BAR_ID} .sb-footer { display:flex; justify-content:space-between; align-items:center; padding-top:6px; border-top:1px solid rgba(255,255,255,0.06); margin-top:4px; font-size:11px; color:rgba(255,255,255,0.35); } #${CONFIG.STATUS_BAR_ID} .sb-clear-btn { cursor:pointer; color:rgba(255,255,255,0.35); transition:color 0.2s; background:none; border:none; font-size:11px; padding:2px 6px; } #${CONFIG.STATUS_BAR_ID} .sb-clear-btn:hover { color:rgba(255,255,255,0.7); } #${CONFIG.STATUS_BAR_ID} .sb-video-dot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:4px; transition:all 0.3s ease; } #${CONFIG.STATUS_BAR_ID} .sb-video-dot.learning { background:#e65100; box-shadow:0 0 8px rgba(230,81,0,0.6); } #${CONFIG.STATUS_BAR_ID} .sb-video-dot.completed { background:#2e7d32; box-shadow:0 0 8px rgba(46,125,50,0.6); } #${CONFIG.STATUS_BAR_ID} .sb-video-dot.waiting { background:#1565c0; box-shadow:0 0 8px rgba(21,101,192,0.6); } #${CONFIG.STATUS_BAR_ID} .sb-video-dot.paused { background:#90a4ae; box-shadow:0 0 8px rgba(144,164,174,0.6); } #${CONFIG.STATUS_BAR_ID} .sb-video-dot.off { background:#ef5350; } `; document.head.appendChild(style); }; const collapse = () => { if (!S.isCollapsed && S.videoExists) { S.isCollapsed = true; S.statusBar.classList.add('collapsed'); } }; const expand = () => { if (S.isCollapsed) { S.isCollapsed = false; S.statusBar.classList.remove('collapsed'); } }; const createStatusBar = () => { if (document.getElementById(CONFIG.STATUS_BAR_ID)) return; const bar = document.createElement('div'); bar.id = CONFIG.STATUS_BAR_ID; bar.innerHTML = `