优化通知功能,增加 toast 消息显示,改善用户体验
This commit is contained in:
parent
f37c6445d1
commit
47077a2b18
|
|
@ -17,8 +17,112 @@
|
|||
const MIN_STUDY_SECONDS = 5 * 60;
|
||||
const VIDEO_SELECTOR = "#vjs_video_3_html5_api";
|
||||
const TEXT_SELECTOR = "#easyXDM_default2872_provider";
|
||||
console.log("插件加载完成,等待界面渲染");
|
||||
const TOAST_CONTAINER_ID = "qingshu-auto-toast-container";
|
||||
const TOAST_STYLE_ID = "qingshu-auto-toast-style";
|
||||
const TOAST_DEFAULT_DURATION = 4000;
|
||||
|
||||
// 创建右下角 toast 容器和样式。
|
||||
// 每次通知都会创建一个新的 toast,所以多个通知会一起堆叠显示。
|
||||
const ensureToastContainer = () => {
|
||||
let container = document.querySelector("#" + TOAST_CONTAINER_ID);
|
||||
|
||||
if (!document.querySelector("#" + TOAST_STYLE_ID)) {
|
||||
const style = document.createElement("style");
|
||||
style.id = TOAST_STYLE_ID;
|
||||
style.textContent = `
|
||||
#${TOAST_CONTAINER_ID} {
|
||||
position: fixed;
|
||||
right: 18px;
|
||||
bottom: 18px;
|
||||
z-index: 2147483647;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
max-width: min(360px, calc(100vw - 36px));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#${TOAST_CONTAINER_ID} .qingshu-toast {
|
||||
box-sizing: border-box;
|
||||
min-width: 220px;
|
||||
max-width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-left: 4px solid #3b82f6;
|
||||
border-radius: 8px;
|
||||
background: rgba(17, 24, 39, 0.94);
|
||||
color: #fff;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.24);
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
transition: opacity 180ms ease, transform 180ms ease;
|
||||
pointer-events: auto;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
#${TOAST_CONTAINER_ID} .qingshu-toast.show {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
#${TOAST_CONTAINER_ID} .qingshu-toast.success {
|
||||
border-left-color: #22c55e;
|
||||
}
|
||||
|
||||
#${TOAST_CONTAINER_ID} .qingshu-toast.warning {
|
||||
border-left-color: #f59e0b;
|
||||
}
|
||||
|
||||
#${TOAST_CONTAINER_ID} .qingshu-toast.error {
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
`;
|
||||
(document.head || document.documentElement).appendChild(style);
|
||||
}
|
||||
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.id = TOAST_CONTAINER_ID;
|
||||
(document.body || document.documentElement).appendChild(container);
|
||||
}
|
||||
|
||||
return container;
|
||||
};
|
||||
|
||||
// 显示一条 toast,同时保留 console 输出。
|
||||
// type 可用 info、success、warning、error;duration 是自动隐藏时间。
|
||||
const notify = (message, type = "info", duration = TOAST_DEFAULT_DURATION) => {
|
||||
const log = type === "error" ? console.error : console.log;
|
||||
log(message);
|
||||
|
||||
const container = ensureToastContainer();
|
||||
const toast = document.createElement("div");
|
||||
toast.className = "qingshu-toast " + type;
|
||||
toast.innerText = message;
|
||||
container.appendChild(toast);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
toast.classList.add("show");
|
||||
});
|
||||
|
||||
const removeToast = () => {
|
||||
toast.classList.remove("show");
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 180);
|
||||
};
|
||||
|
||||
toast.addEventListener("click", removeToast);
|
||||
setTimeout(removeToast, duration);
|
||||
};
|
||||
|
||||
notify("插件加载完成,等待界面渲染", "info");
|
||||
|
||||
// 等待某个 DOM 条件成立。
|
||||
// 页面里的课程列表、视频、文本课件经常是后加载的,所以不用固定延迟,
|
||||
// 而是监听 DOM 变化,直到 getResult() 返回有效结果。
|
||||
const waitForCondition = (getResult) => {
|
||||
const result = getResult();
|
||||
|
||||
|
|
@ -43,6 +147,8 @@
|
|||
});
|
||||
};
|
||||
|
||||
// 把“共4分钟49秒”“共1小时2分钟3秒”这类文字换算成秒数,
|
||||
// 方便判断是否已经学满 5 分钟。
|
||||
const parseStudySeconds = (text) => {
|
||||
const hourMatch = text.match(/(\d+)\s*小时/);
|
||||
const minuteMatch = text.match(/(\d+)\s*分钟/);
|
||||
|
|
@ -55,6 +161,10 @@
|
|||
);
|
||||
};
|
||||
|
||||
// 判断一个课程节点是否需要进入:
|
||||
// 1. 没有 study_being,说明还没学过,需要进入;
|
||||
// 2. 学过但累计时间低于 5 分钟,也需要再次进入;
|
||||
// 3. 学习时间达到 5 分钟及以上,就跳过。
|
||||
const shouldEnterCourseware = (item) => {
|
||||
const mark = item.querySelector(".study_being");
|
||||
|
||||
|
|
@ -66,6 +176,8 @@
|
|||
return studiedSeconds < MIN_STUDY_SECONDS;
|
||||
};
|
||||
|
||||
// 返回课程列表页。
|
||||
// 优先点击页面自带的返回按钮;如果没找到,就用浏览器历史记录兜底。
|
||||
const returnToStudyPage = () => {
|
||||
const backLink = document.querySelector(".back-link");
|
||||
|
||||
|
|
@ -77,6 +189,8 @@
|
|||
history.back();
|
||||
};
|
||||
|
||||
// 播放页可能是视频课件,也可能是文本课件。
|
||||
// 这里会一直等到其中一种课件元素真正出现在页面上。
|
||||
const waitForCoursewareElement = () =>
|
||||
waitForCondition(
|
||||
() =>
|
||||
|
|
@ -85,10 +199,12 @@
|
|||
);
|
||||
|
||||
const onPageLoad = async () => {
|
||||
console.log("页面加载完成,开始执行脚本");
|
||||
notify("页面加载完成,开始执行脚本", "info");
|
||||
|
||||
if (url.includes("CourseStudy")) {
|
||||
console.log("进入准备阶段");
|
||||
notify("进入准备阶段,等待课程列表", "info");
|
||||
|
||||
// 课程列表是异步渲染的,等到至少有一个课程节点后再开始遍历。
|
||||
const items = await waitForCondition(() => {
|
||||
const list = document.querySelector("#list");
|
||||
const items = list ? list.querySelectorAll("li > a.node") : [];
|
||||
|
|
@ -96,60 +212,72 @@
|
|||
return items.length > 0 ? items : null;
|
||||
});
|
||||
|
||||
console.log("获取到课程数量:", items.length);
|
||||
notify("获取到课程数量:" + items.length, "success");
|
||||
|
||||
// 从上到下找到第一个“没学过”或“学习不足5分钟”的课件并进入。
|
||||
for (const item of items) {
|
||||
if (shouldEnterCourseware(item)) {
|
||||
const mark = item.querySelector(".study_being");
|
||||
const studiedSeconds = mark ? parseStudySeconds(mark.innerText) : 0;
|
||||
console.log(
|
||||
notify(
|
||||
"点击未完成学习时长的视频:" +
|
||||
item.querySelector(".title").innerText +
|
||||
",已学习:" +
|
||||
studiedSeconds +
|
||||
"秒"
|
||||
"秒",
|
||||
"success",
|
||||
6000
|
||||
);
|
||||
item.click();
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.log("所有视频均已播放完毕,结束脚本");
|
||||
notify("所有视频均已播放完毕,结束脚本", "success", 6000);
|
||||
} else if (url.includes("CourseShow")) {
|
||||
console.log("进入学习阶段");
|
||||
// 学习阶段
|
||||
notify("进入学习阶段,等待课件加载", "info");
|
||||
|
||||
// 等到播放页真正渲染出视频或文本课件,再决定如何处理。
|
||||
const coursewareElement = await waitForCoursewareElement();
|
||||
|
||||
// 文本课件没有 video 标签,不需要播放;停留 5 分钟后返回列表页。
|
||||
if (coursewareElement.matches(TEXT_SELECTOR)) {
|
||||
console.log("检测到文本课件,等待5分钟后返回准备页面");
|
||||
notify("检测到文本课件,等待5分钟后返回准备页面", "warning", 6000);
|
||||
setTimeout(() => {
|
||||
console.log("文本课件等待完成,返回准备页面");
|
||||
notify("文本课件等待完成,返回准备页面", "success");
|
||||
returnToStudyPage();
|
||||
}, MIN_STUDY_SECONDS * 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
// 视频课件需要周期性检查播放状态:启动播放、设置倍速、播完返回。
|
||||
let videoMissingNotified = false;
|
||||
const videoInterval = setInterval(() => {
|
||||
const video = document.querySelector(VIDEO_SELECTOR);
|
||||
|
||||
// 有些播放器会重建 video 元素,短暂找不到时继续等下一轮。
|
||||
if (!video) {
|
||||
console.log("视频元素暂时不可用,继续等待");
|
||||
if (!videoMissingNotified) {
|
||||
notify("视频元素暂时不可用,继续等待", "warning");
|
||||
videoMissingNotified = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
videoMissingNotified = false;
|
||||
|
||||
if (video.paused && video.currentTime === 0) {
|
||||
video.play();
|
||||
console.log("视频未播放,开始播放视频");
|
||||
notify("视频未播放,开始播放视频", "success");
|
||||
return;
|
||||
}
|
||||
|
||||
if (video.currentTime >= video.duration - 1) {
|
||||
console.log("视频播放结束,返回准备页面");
|
||||
notify("视频播放结束,返回准备页面", "success");
|
||||
clearInterval(videoInterval);
|
||||
returnToStudyPage();
|
||||
}
|
||||
|
||||
if (video.playbackRate === 1) {
|
||||
console.log("将视频倍速设置为2倍速");
|
||||
notify("将视频倍速设置为2倍速", "success");
|
||||
video.playbackRate = 2;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue