// ==UserScript== // @name Welearn 时长精灵 // @namespace http://tampermonkey.net/ // @version 39.7 // @description 具备智能时长控制,保证时长有效性,计算时长数据的小工具 // @author Assistant // @match *://welearn.sflep.com/student/course_info.aspx* // @match *://welearn.sflep.com/student/StudyCourse.aspx* // @match *://centercourseware.sflep.com/* // @grant none // @run-at document-end // ==/UserScript== (function() { 'use strict'; const DB_KEY = 'WL_DATA_V39'; const TASK_KEY = 'WL_TASK_V39_6'; const UI_KEY = 'WL_UI_POS_V39'; const HOST = window.location.hostname; const IS_TOP = (window.self === window.top); const style = document.createElement('style'); style.textContent = ` /* 主面板基础样式 */ #wl-panel { position: fixed; width: 340px; max-height: 90vh; background: rgba(20,20,20,0.98); color: #e0e0e0; border: 1px solid #444; z-index: 2147483647; border-radius: 12px; font-family: 'Microsoft YaHei', sans-serif; display: flex; flex-direction: column; box-shadow: 0 10px 50px rgba(0,0,0,0.8); backdrop-filter: blur(5px); transition: height 0.3s, background-color 0.3s, border-color 0.3s, box-shadow 0.3s; } /* 1. 赞助小圆按钮 */ .wl-donate-btn { display: block; margin: 0 auto 8px; /* 居中并与下方文字保持间距 */ width: 30px; height: 30px; line-height: 30px; background: linear-gradient(135deg, #89f7fe, #66a6ff); /* 浅蓝流光色 */ color: #fff; border-radius: 50%; font-size: 14px; font-weight: bold; cursor: pointer; box-shadow: 0 2px 10px rgba(102, 166, 255, 0.4); transition: 0.2s; } /* 2. 弹窗背后的半透明遮罩 */ #wl-donate-mask { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); /* 深色半透明 */ backdrop-filter: blur(5px); /* 背景模糊,显高级 */ display: none; /* 默认隐藏 */ justify-content: center; align-items: center; z-index: 2147483648; /* 确保在最前面 */ } /* 3. 存放图片的卡片 */ .wl-donate-card { background: #1a1a1a; padding: 25px; border-radius: 16px; border: 1px solid #555; text-align: center; position: relative; } .wl-donate-card img { width: 200px; height: 200px; border-radius: 8px; margin: 10px 0; } /* === 主题变色 (恶搞模式) === */ #wl-panel.wl-theme-fun { border-color: #ff00cc; background: rgba(30, 10, 30, 0.98); } #wl-panel.wl-theme-fun .wl-header { background: rgba(255, 0, 200, 0.1); border-bottom-color: #ff00cc; } #wl-panel.wl-theme-fun .wl-status-text { color: #ff00cc !important; } #wl-panel.wl-theme-fun .wl-hl { color: #00ffff !important; } #wl-panel.wl-theme-fun input[type=number]:focus, #wl-panel.wl-theme-fun .wl-input-group:focus-within { border-color: #ff00cc !important; } #wl-panel.wl-theme-fun .wl-running { color: #ff00cc !important; animation: wl-pulse-fun 1s infinite !important; } #wl-panel.wl-theme-fun .wl-sec-row.active { border-left-color: #ff00cc; background: rgba(255,0,200,0.1); } #wl-panel.wl-theme-fun .wl-checkbox { accent-color: #ff00cc; } #wl-panel.wl-theme-fun #wl-btn-run { background: linear-gradient(45deg, #ff00cc, #333399) !important; } /* 最小化状态 */ #wl-panel.wl-minimized { width: 180px !important; height: auto !important; max-height: 50px !important; border-radius: 25px; border: 1px solid #555; overflow: hidden; } #wl-panel.wl-minimized .wl-body, #wl-panel.wl-minimized .wl-footer-area, #wl-panel.wl-minimized .wl-setting-box, #wl-panel.wl-minimized .wl-filter-box, #wl-panel.wl-minimized .wl-timer-full, #wl-panel.wl-minimized .wl-main-btn { display: none !important; } #wl-panel.wl-minimized .wl-header { border:none; background: transparent; padding: 10px 15px; cursor: move; } #wl-panel.wl-minimized .wl-status-bar { margin: 0; justify-content: center; } /* 头部区域 */ .wl-header { padding:15px; border-bottom:1px solid #333; background:rgba(255,255,255,0.05); flex-shrink:0; border-radius:12px 12px 0 0; cursor: move; user-select: none; transition: background-color 0.3s; } .wl-ctrl-btns { display: flex; align-items: center; gap: 8px; } .wl-icon-btn { cursor: pointer; color: #888; font-weight: bold; font-size: 14px; transition: 0.2s; padding: 2px 5px; } .wl-icon-btn:hover { color: #fff; background: #444; border-radius: 4px; } /* 滚动区域 */ .wl-body { overflow-y:auto; flex-grow:1; padding:0; scrollbar-width: thin; scrollbar-color: #444 #222; } .wl-row { display:flex; align-items:center; margin-bottom:8px; font-size:13px; justify-content: space-between; } .wl-status-bar { display:flex; justify-content:space-between; align-items:center; } .wl-setting-box { background:#2a2a2a; padding:12px 15px; border-radius:8px; margin-bottom:8px; border:1px solid #333; } .wl-setting-box.small { padding: 8px 12px; background:#222; border-color:#333; } .wl-big-text { font-size:14px; font-weight:bold; color:#ddd; width: 80px; } .wl-small-text { font-size:12px; color:#aaa; margin-right: 5px; } /* 输入框组合 */ .wl-input-group { display: flex; align-items: center; background: #111; border-radius: 6px; border: 1px solid #444; padding: 2px; } .wl-input-group:focus-within { border-color: #0f0; } .wl-spin-btn { width: 24px; height: 24px; background: transparent; border: none; color: #888; cursor: pointer; font-size: 16px; line-height: 1; display: flex; align-items: center; justify-content: center; transition: 0.2s; border-radius: 4px; } .wl-spin-btn:hover { background: #333; color: #fff; } input[type=number]::-webkit-inner-spin-button, input[type=number]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; } input[type=number] { background:transparent; border:none; color:#fff; text-align:center; font-size:15px; font-weight:bold; outline:none; width: 50px; height: 24px; -moz-appearance: textfield; } .wl-btn { border:none; border-radius:4px; padding:4px 10px; cursor:pointer; font-size:12px; color:#fff; transition:0.2s; font-weight:bold; } .wl-btn:hover { opacity:0.8; } .wl-btn-main { width:100%; font-size:16px; padding:10px; border-radius:6px; letter-spacing:1px; margin-top:5px; text-shadow:0 1px 2px rgba(0,0,0,0.5); } .wl-unit { border-bottom:1px solid #333; } .wl-unit-bar { padding:10px 12px; background:#181818; cursor:pointer; display:flex; align-items:center; font-size:13px; transition:0.2s; } .wl-unit-bar:hover { background:#222; } .wl-sec-row { padding:8px 12px 8px 35px; border-left:3px solid transparent; display:flex; align-items:center; font-size:12px; border-bottom:1px solid #222; background:#111; } .wl-sec-row.active { border-left-color:#0f0; background:rgba(0,255,0,0.05); } .wl-hl { color:#0f0; font-weight:bold; font-family:monospace; font-size:14px; } .wl-checkbox { margin-right:10px; cursor:pointer; width:16px; height:16px; accent-color: #0f0; } /* [修改] 底部署名样式 - 浅蓝流光渐变 */ .wl-footer-area { background:#111; padding:8px; border-radius:0 0 12px 12px; text-align:center; cursor:pointer; user-select:none; border-top: 1px solid #222; } .wl-footer-text { font-size: 13px; font-weight: 900; letter-spacing: 1px; /* 从浅蓝开始,过渡到紫、粉,再回到浅蓝 */ background: linear-gradient(90deg, #89f7fe, #66a6ff, #af40ff, #ff5e62, #89f7fe); background-size: 300% 100%; -webkit-background-clip: text; color: transparent; animation: wl-blue-shift 6s linear infinite; display: inline-block; transition: transform 0.2s; } .wl-footer-area:active .wl-footer-text { transform: scale(0.95); } /* 动画关键帧 */ @keyframes wl-pulse { 0% { opacity:0.6; color:#4dff4d; } 50% { opacity:1; color:#fff; } 100% { opacity:0.6; color:#4dff4d; } } @keyframes wl-pulse-fun { 0% { opacity:0.6; color:#ff00cc; } 50% { opacity:1; color:#fff; } 100% { opacity:0.6; color:#ff00cc; } } /* [修改] 新的浅蓝渐变动画 */ @keyframes wl-blue-shift { 0% { background-position: 0% 50%; } 100% { background-position: 100% 50%; } } /* 表情粒子动画 */ .wl-emoji-particle { position: fixed; pointer-events: none; font-size: 24px; animation: wl-emoji-pop 0.8s cubic-bezier(0.18, 0.89, 0.32, 1.28) forwards; z-index: 2147483648; } @keyframes wl-emoji-pop { 0% { transform: translate(-50%, -50%) scale(0.5); opacity: 0; } 50% { opacity: 1; } 100% { transform: translate(-50%, -80px) scale(1.2); opacity: 0; } } .wl-body::-webkit-scrollbar { width: 6px; } .wl-body::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; } `; document.head.appendChild(style); const t2s = (str) => { if (!str) return 0; let s = str.replace(/用时|\s/g, ''); let p = s.split(':').map(Number); return p.length === 3 ? p[0]*3600+p[1]*60+p[2] : (p.length === 2 ? p[0]*60+p[1] : 0); }; const s2t = (s) => { if (isNaN(s) || s < 0) return "00:00"; s = Math.floor(s); let h = Math.floor(s/3600), m = Math.floor((s%3600)/60), sec = s%60; return (h > 0 ? h + ":" : "") + m.toString().padStart(2,'0') + ":" + sec.toString().padStart(2,'0'); }; if (HOST.includes('centercourseware')) { setInterval(() => { const live = window.sco_time || (window.CourseAPI && window.CourseAPI.getTime()); if (live) window.parent.postMessage({ type: 'WL_TICK', val: parseInt(live) }, '*'); }, 1000); return; } if (IS_TOP) { let state = { currentId: "", sessionInc: 0, expandedUnits: new Set(), selectedIds: new Set(), addMin: 2, floatMin: 0, timeUnit: 'min', currentRandomTarget: 0, filterMin: 9999, isAuto: false, taskStartTime: 0, lastSyncCount: 0, isMinimized: false, posX: 'right: 15px', posY: 'top: 15px', isFunTheme: false }; try { const saved = JSON.parse(localStorage.getItem(TASK_KEY) || "{}"); if (saved.selectedIds) state.selectedIds = new Set(saved.selectedIds); if (saved.addMin) state.addMin = saved.addMin; if (saved.floatMin) state.floatMin = saved.floatMin; if (saved.timeUnit) state.timeUnit = saved.timeUnit; if (saved.isAuto) state.isAuto = saved.isAuto; state.taskStartTime = 0; state.currentRandomTarget = 0; } catch(e) {} try { const uiSaved = JSON.parse(localStorage.getItem(UI_KEY) || "{}"); if (uiSaved.posX) state.posX = uiSaved.posX; if (uiSaved.posY) state.posY = uiSaved.posY; if (uiSaved.isMinimized !== undefined) state.isMinimized = uiSaved.isMinimized; if (uiSaved.isFunTheme !== undefined) state.isFunTheme = uiSaved.isFunTheme; } catch(e) {} const saveState = () => localStorage.setItem(TASK_KEY, JSON.stringify({ selectedIds: Array.from(state.selectedIds), addMin: state.addMin, floatMin: state.floatMin, timeUnit: state.timeUnit, isAuto: state.isAuto })); const saveUIState = () => localStorage.setItem(UI_KEY, JSON.stringify({ posX: state.posX, posY: state.posY, isMinimized: state.isMinimized, isFunTheme: state.isFunTheme })); function scrape() { let freshData = {}; let count = 0; const items = document.querySelectorAll('li[onclick*="StartSCO"], li[id^="liSCO_"]'); items.forEach(li => { let id = null; if (li.id && li.id.startsWith('liSCO_')) { id = li.id.replace('liSCO_', ''); } else { let clickStr = li.getAttribute('onclick') || ""; let m = clickStr.match(/['"]([^'"]+)['"]/); if (m) id = m[1]; } if (id) { let spans = li.querySelectorAll('span.pull-right'); let timeText = ""; spans.forEach(sp => { let txt = sp.textContent; if (txt.includes('用时') && !sp.style.display.includes('none')) { timeText = txt.replace('用时', '').trim(); } }); if (!timeText) { spans.forEach(sp => { let txt = sp.textContent; if (txt.includes(':') && !sp.style.display.includes('none')) { timeText = txt.trim(); } }); } if (timeText) { freshData[id] = timeText; count++; } } }); if (count > 0) { localStorage.setItem(DB_KEY, JSON.stringify(freshData)); state.lastSyncCount = count; } } window.addEventListener('message', e => { if (e.data.type === 'WL_TICK') state.sessionInc = e.data.val; }); function makeDraggable(el) { let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; const header = el.querySelector(".wl-header"); header.onmousedown = dragMouseDown; function dragMouseDown(e) { e = e || window.event; if (['INPUT', 'BUTTON'].includes(e.target.tagName) || e.target.closest('.wl-icon-btn') || e.target.closest('.wl-spin-btn') || e.target.closest('.wl-input-group')) return; e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; } function elementDrag(e) { e = e || window.event; e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; el.style.top = (el.offsetTop - pos2) + "px"; el.style.left = (el.offsetLeft - pos1) + "px"; el.style.right = 'auto'; } function closeDragElement() { document.onmouseup = null; document.onmousemove = null; state.posX = `left: ${el.style.left}`; state.posY = `top: ${el.style.top}`; saveUIState(); } } function initPanel() { if (document.getElementById('wl-panel')) return; const div = document.createElement('div'); div.id = 'wl-panel'; div.style.cssText = `${state.posX}; ${state.posY};`; if (state.isMinimized) div.classList.add('wl-minimized'); if (state.isFunTheme) div.classList.add('wl-theme-fun'); div.innerHTML = `