From a9019147941eb0ced78bf38b252fee6847de5298 Mon Sep 17 00:00:00 2001 From: jinglekang Date: Fri, 5 Jun 2026 14:17:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=84=9A=E6=9C=AC=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8F=B7=EF=BC=8C=E5=A2=9E=E5=8A=A0=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=A1=B9=E6=94=AF=E6=8C=81=EF=BC=8C=E4=BC=98=E5=8C=96=E5=AD=A6?= =?UTF-8?q?=E4=B9=A0=E6=97=B6=E9=95=BF=E5=92=8C=E8=A7=86=E9=A2=91=E5=80=8D?= =?UTF-8?q?=E9=80=9F=E8=AE=BE=E7=BD=AE=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- qingshuxuetang_auto_play.user.js | 204 +++++++++++++++++++++++++------ 1 file changed, 167 insertions(+), 37 deletions(-) diff --git a/qingshuxuetang_auto_play.user.js b/qingshuxuetang_auto_play.user.js index d82c124..7a3241a 100644 --- a/qingshuxuetang_auto_play.user.js +++ b/qingshuxuetang_auto_play.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name 平顶山学院(青书学堂)自动学习脚本 // @namespace http://tampermonkey.net/ -// @version 2026-06-05.2 +// @version 2026-06-05.3 // @description try to take over the world! // @author You // @match https://degree.qingshuxuetang.com/pdsu/Student/Course/CourseStudy* @@ -9,7 +9,10 @@ // @icon https://www.google.com/s2/favicons?sz=64&domain=qingshuxuetang.com // @updateURL https://git.zaobai.com/public/tampermonkey_script/raw/branch/master/qingshuxuetang_auto_play.user.js // @downloadURL https://git.zaobai.com/public/tampermonkey_script/raw/branch/master/qingshuxuetang_auto_play.user.js -// @grant none +// @grant GM_getValue +// @grant GM_setValue +// @grant GM_deleteValue +// @grant GM_registerMenuCommand // ==/UserScript== (function () { @@ -18,34 +21,48 @@ // 当前页面地址,用来判断是在课程列表页还是课件播放页。 const url = window.location.href; - // 课件至少需要学习多少秒;默认 5 分钟。 - const MIN_STUDY_SECONDS = 5 * 60; - // 视频课件的 video 标签选择器;如果网站改版导致找不到视频,优先检查这里。 const VIDEO_SELECTOR = "#vjs_video_3_html5_api"; - // 视频默认播放倍速;想改成 1.5 倍、3 倍就改这个值。 - const VIDEO_PLAYBACK_RATE = 2; - // 文本课件容器选择器;easyXDM 中间的数字会变,所以这里用开头和结尾匹配。 const TEXT_SELECTOR = "[id^='easyXDM_default'][id$='_provider']"; + // Tampermonkey 持久化配置的存储 key。 + // 配置会存在脚本自己的存储区里,更新脚本代码不会覆盖这里的值。 + const CONFIG_STORAGE_KEY = "qingshu_auto_config"; + + // 默认配置。第一次安装或重置配置后,会使用这些值。 + const DEFAULT_CONFIG = { + // 课件至少需要学习多少秒;默认 5 分钟。 + minStudySeconds: 5 * 60, + + // 视频默认播放倍速;想改成 1.5 倍、3 倍可以从 Tampermonkey 菜单配置。 + videoPlaybackRate: 2, + + // toast 默认显示时长,单位毫秒;4000 表示 4 秒后自动隐藏。 + toastDefaultDuration: 4000, + + // 运行心跳提示间隔,单位秒;视频播放中、文本课件等待中都会按这个间隔提醒。 + heartbeatSeconds: 10, + + // 课程列表最多等待多少秒;超过后认为可能请求失败,触发刷新。 + courseListWaitSeconds: 60, + + // 课程列表加载失败时最多自动刷新几次。 + maxCourseListRefreshCount: 3, + }; + + // 读取 Tampermonkey 里保存过的配置,并和默认配置合并。 + // 以后脚本新增配置项时,旧配置也能自动补上默认值。 + const CONFIG = { + ...DEFAULT_CONFIG, + ...GM_getValue(CONFIG_STORAGE_KEY, {}), + }; + // toast 容器和样式标签的 id,通常不用改,除非和页面元素冲突。 const TOAST_CONTAINER_ID = "qingshu-auto-toast-container"; const TOAST_STYLE_ID = "qingshu-auto-toast-style"; - // toast 默认显示时长,单位毫秒;4000 表示 4 秒后自动隐藏。 - const TOAST_DEFAULT_DURATION = 4000; - - // 运行心跳提示间隔,单位秒;视频播放中、文本课件等待中都会按这个间隔提醒。 - const HEARTBEAT_SECONDS = 10; - - // 课程列表最多等待多少秒;超过后认为可能请求失败,触发刷新。 - const COURSE_LIST_WAIT_SECONDS = 60; - - // 课程列表加载失败时最多自动刷新几次。 - const MAX_COURSE_LIST_REFRESH_COUNT = 3; - // 记录课程列表自动刷新次数的 sessionStorage key。 // 带上 pathname/search,避免不同课程页面之间互相影响。 const COURSE_LIST_REFRESH_KEY = @@ -125,7 +142,11 @@ // 显示一条 toast,同时保留 console 输出。 // type 可用 info、success、warning、error;duration 是自动隐藏时间。 - const notify = (message, type = "info", duration = TOAST_DEFAULT_DURATION) => { + const notify = ( + message, + type = "info", + duration = CONFIG.toastDefaultDuration + ) => { const log = type === "error" ? console.error : console.log; log(message); @@ -150,6 +171,99 @@ setTimeout(removeToast, duration); }; + const saveConfig = (patch) => { + Object.assign(CONFIG, patch); + GM_setValue(CONFIG_STORAGE_KEY, CONFIG); + notify("配置已保存,刷新页面后完全生效", "success", 6000); + }; + + const promptNumberConfig = (label, key, unit, currentValue, toStoredValue) => { + const input = prompt(label + "(当前:" + currentValue + unit + ")", currentValue); + + if (input === null) { + return; + } + + const value = Number(input); + + if (!Number.isFinite(value) || value <= 0) { + notify("配置失败:请输入大于0的数字", "error", 6000); + return; + } + + saveConfig({ + [key]: toStoredValue ? toStoredValue(value) : value, + }); + }; + + // 在 Tampermonkey 菜单里提供配置入口。 + // 这些值保存在脚本存储区,脚本更新不会覆盖它们。 + const registerConfigMenus = () => { + GM_registerMenuCommand("设置视频倍速", () => { + promptNumberConfig( + "设置视频倍速", + "videoPlaybackRate", + "倍", + CONFIG.videoPlaybackRate + ); + }); + + GM_registerMenuCommand("设置最小学习时间", () => { + promptNumberConfig( + "设置最小学习时间", + "minStudySeconds", + "分钟", + CONFIG.minStudySeconds / 60, + (value) => value * 60 + ); + }); + + GM_registerMenuCommand("设置心跳提示间隔", () => { + promptNumberConfig( + "设置心跳提示间隔", + "heartbeatSeconds", + "秒", + CONFIG.heartbeatSeconds + ); + }); + + GM_registerMenuCommand("设置课程列表等待时间", () => { + promptNumberConfig( + "设置课程列表等待时间", + "courseListWaitSeconds", + "秒", + CONFIG.courseListWaitSeconds + ); + }); + + GM_registerMenuCommand("设置课程列表最大刷新次数", () => { + promptNumberConfig( + "设置课程列表最大刷新次数", + "maxCourseListRefreshCount", + "次", + CONFIG.maxCourseListRefreshCount, + Math.floor + ); + }); + + GM_registerMenuCommand("设置 toast 显示时长", () => { + promptNumberConfig( + "设置 toast 显示时长", + "toastDefaultDuration", + "秒", + CONFIG.toastDefaultDuration / 1000, + (value) => value * 1000 + ); + }); + + GM_registerMenuCommand("重置脚本配置", () => { + GM_deleteValue(CONFIG_STORAGE_KEY); + Object.assign(CONFIG, DEFAULT_CONFIG); + notify("配置已重置,刷新页面后完全生效", "success", 6000); + }); + }; + + registerConfigMenus(); notify("插件加载完成,等待界面渲染", "info"); // 等待某个 DOM 条件成立。 @@ -201,9 +315,11 @@ const refreshCourseStudyIfNeeded = () => { const refreshCount = Number(sessionStorage.getItem(COURSE_LIST_REFRESH_KEY) || 0); - if (refreshCount >= MAX_COURSE_LIST_REFRESH_COUNT) { + if (refreshCount >= CONFIG.maxCourseListRefreshCount) { notify( - "课程列表等待超时,已刷新3次仍未加载,停止自动刷新", + "课程列表等待超时,已刷新" + + CONFIG.maxCourseListRefreshCount + + "次仍未加载,停止自动刷新", "error", 8000 ); @@ -213,10 +329,12 @@ const nextRefreshCount = refreshCount + 1; sessionStorage.setItem(COURSE_LIST_REFRESH_KEY, String(nextRefreshCount)); notify( - "课程列表等待超过1分钟,准备刷新页面(第" + + "课程列表等待超过" + + formatSeconds(CONFIG.courseListWaitSeconds) + + ",准备刷新页面(第" + nextRefreshCount + "/" + - MAX_COURSE_LIST_REFRESH_COUNT + + CONFIG.maxCourseListRefreshCount + "次)", "warning", 8000 @@ -254,7 +372,7 @@ } const studiedSeconds = parseStudySeconds(mark.innerText); - return studiedSeconds < MIN_STUDY_SECONDS; + return studiedSeconds < CONFIG.minStudySeconds; }; // 返回课程列表页。 @@ -305,7 +423,7 @@ return items.length > 0 ? items : null; }, - COURSE_LIST_WAIT_SECONDS * 1000 + CONFIG.courseListWaitSeconds * 1000 ); if (!items) { @@ -317,7 +435,7 @@ notify("获取到课程数量:" + items.length, "success"); - // 从上到下找到第一个“没学过”或“学习不足5分钟”的课件并进入。 + // 从上到下找到第一个“没学过”或“学习不足配置时长”的课件并进入。 for (const item of items) { if (shouldEnterCourseware(item)) { const mark = item.querySelector(".study_being"); @@ -342,16 +460,22 @@ // 等到播放页真正渲染出视频或文本课件,再决定如何处理。 const coursewareElement = await waitForCoursewareElement(); - // 文本课件没有 video 标签,不需要播放;停留 5 分钟后返回列表页。 + // 文本课件没有 video 标签,不需要播放;停留到配置的学习时长后返回列表页。 if (coursewareElement.matches(TEXT_SELECTOR)) { - notify("检测到文本课件,等待5分钟后返回准备页面", "warning", 6000); + notify( + "检测到文本课件,等待" + + formatSeconds(CONFIG.minStudySeconds) + + "后返回准备页面", + "warning", + 6000 + ); const textStartedAt = Date.now(); let textInterval; const notifyTextProgress = () => { const studiedSeconds = Math.floor((Date.now() - textStartedAt) / 1000); - if (studiedSeconds >= MIN_STUDY_SECONDS) { + if (studiedSeconds >= CONFIG.minStudySeconds) { clearInterval(textInterval); notify("文本课件等待完成,返回准备页面", "success"); returnToStudyPage(); @@ -362,14 +486,14 @@ "文本课件学习中,已等待:" + formatSeconds(studiedSeconds) + ",剩余:" + - formatSeconds(MIN_STUDY_SECONDS - studiedSeconds), + formatSeconds(CONFIG.minStudySeconds - studiedSeconds), "info" ); }; textInterval = setInterval( notifyTextProgress, - HEARTBEAT_SECONDS * 1000 + CONFIG.heartbeatSeconds * 1000 ); notifyTextProgress(); return; @@ -403,12 +527,18 @@ returnToStudyPage(); } - if (video.playbackRate !== VIDEO_PLAYBACK_RATE) { - notify("将视频倍速设置为" + VIDEO_PLAYBACK_RATE + "倍速", "success"); - video.playbackRate = VIDEO_PLAYBACK_RATE; + if (video.playbackRate !== CONFIG.videoPlaybackRate) { + notify( + "将视频倍速设置为" + CONFIG.videoPlaybackRate + "倍速", + "success" + ); + video.playbackRate = CONFIG.videoPlaybackRate; } - if (Date.now() - lastVideoHeartbeatAt >= HEARTBEAT_SECONDS * 1000) { + if ( + Date.now() - lastVideoHeartbeatAt >= + CONFIG.heartbeatSeconds * 1000 + ) { lastVideoHeartbeatAt = Date.now(); notify( "视频正在播放,当前:" +