优化通知功能,增加 toast 消息显示,改善用户体验
This commit is contained in:
parent
f37c6445d1
commit
47077a2b18
|
|
@ -17,8 +17,112 @@
|
||||||
const MIN_STUDY_SECONDS = 5 * 60;
|
const MIN_STUDY_SECONDS = 5 * 60;
|
||||||
const VIDEO_SELECTOR = "#vjs_video_3_html5_api";
|
const VIDEO_SELECTOR = "#vjs_video_3_html5_api";
|
||||||
const TEXT_SELECTOR = "#easyXDM_default2872_provider";
|
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 waitForCondition = (getResult) => {
|
||||||
const result = getResult();
|
const result = getResult();
|
||||||
|
|
||||||
|
|
@ -43,6 +147,8 @@
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 把“共4分钟49秒”“共1小时2分钟3秒”这类文字换算成秒数,
|
||||||
|
// 方便判断是否已经学满 5 分钟。
|
||||||
const parseStudySeconds = (text) => {
|
const parseStudySeconds = (text) => {
|
||||||
const hourMatch = text.match(/(\d+)\s*小时/);
|
const hourMatch = text.match(/(\d+)\s*小时/);
|
||||||
const minuteMatch = 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 shouldEnterCourseware = (item) => {
|
||||||
const mark = item.querySelector(".study_being");
|
const mark = item.querySelector(".study_being");
|
||||||
|
|
||||||
|
|
@ -66,6 +176,8 @@
|
||||||
return studiedSeconds < MIN_STUDY_SECONDS;
|
return studiedSeconds < MIN_STUDY_SECONDS;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 返回课程列表页。
|
||||||
|
// 优先点击页面自带的返回按钮;如果没找到,就用浏览器历史记录兜底。
|
||||||
const returnToStudyPage = () => {
|
const returnToStudyPage = () => {
|
||||||
const backLink = document.querySelector(".back-link");
|
const backLink = document.querySelector(".back-link");
|
||||||
|
|
||||||
|
|
@ -77,6 +189,8 @@
|
||||||
history.back();
|
history.back();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 播放页可能是视频课件,也可能是文本课件。
|
||||||
|
// 这里会一直等到其中一种课件元素真正出现在页面上。
|
||||||
const waitForCoursewareElement = () =>
|
const waitForCoursewareElement = () =>
|
||||||
waitForCondition(
|
waitForCondition(
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -85,10 +199,12 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
const onPageLoad = async () => {
|
const onPageLoad = async () => {
|
||||||
console.log("页面加载完成,开始执行脚本");
|
notify("页面加载完成,开始执行脚本", "info");
|
||||||
|
|
||||||
if (url.includes("CourseStudy")) {
|
if (url.includes("CourseStudy")) {
|
||||||
console.log("进入准备阶段");
|
notify("进入准备阶段,等待课程列表", "info");
|
||||||
|
|
||||||
|
// 课程列表是异步渲染的,等到至少有一个课程节点后再开始遍历。
|
||||||
const items = await waitForCondition(() => {
|
const items = await waitForCondition(() => {
|
||||||
const list = document.querySelector("#list");
|
const list = document.querySelector("#list");
|
||||||
const items = list ? list.querySelectorAll("li > a.node") : [];
|
const items = list ? list.querySelectorAll("li > a.node") : [];
|
||||||
|
|
@ -96,60 +212,72 @@
|
||||||
return items.length > 0 ? items : null;
|
return items.length > 0 ? items : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("获取到课程数量:", items.length);
|
notify("获取到课程数量:" + items.length, "success");
|
||||||
|
|
||||||
|
// 从上到下找到第一个“没学过”或“学习不足5分钟”的课件并进入。
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (shouldEnterCourseware(item)) {
|
if (shouldEnterCourseware(item)) {
|
||||||
const mark = item.querySelector(".study_being");
|
const mark = item.querySelector(".study_being");
|
||||||
const studiedSeconds = mark ? parseStudySeconds(mark.innerText) : 0;
|
const studiedSeconds = mark ? parseStudySeconds(mark.innerText) : 0;
|
||||||
console.log(
|
notify(
|
||||||
"点击未完成学习时长的视频:" +
|
"点击未完成学习时长的视频:" +
|
||||||
item.querySelector(".title").innerText +
|
item.querySelector(".title").innerText +
|
||||||
",已学习:" +
|
",已学习:" +
|
||||||
studiedSeconds +
|
studiedSeconds +
|
||||||
"秒"
|
"秒",
|
||||||
|
"success",
|
||||||
|
6000
|
||||||
);
|
);
|
||||||
item.click();
|
item.click();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log("所有视频均已播放完毕,结束脚本");
|
notify("所有视频均已播放完毕,结束脚本", "success", 6000);
|
||||||
} else if (url.includes("CourseShow")) {
|
} else if (url.includes("CourseShow")) {
|
||||||
console.log("进入学习阶段");
|
notify("进入学习阶段,等待课件加载", "info");
|
||||||
// 学习阶段
|
|
||||||
|
// 等到播放页真正渲染出视频或文本课件,再决定如何处理。
|
||||||
const coursewareElement = await waitForCoursewareElement();
|
const coursewareElement = await waitForCoursewareElement();
|
||||||
|
|
||||||
|
// 文本课件没有 video 标签,不需要播放;停留 5 分钟后返回列表页。
|
||||||
if (coursewareElement.matches(TEXT_SELECTOR)) {
|
if (coursewareElement.matches(TEXT_SELECTOR)) {
|
||||||
console.log("检测到文本课件,等待5分钟后返回准备页面");
|
notify("检测到文本课件,等待5分钟后返回准备页面", "warning", 6000);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log("文本课件等待完成,返回准备页面");
|
notify("文本课件等待完成,返回准备页面", "success");
|
||||||
returnToStudyPage();
|
returnToStudyPage();
|
||||||
}, MIN_STUDY_SECONDS * 1000);
|
}, MIN_STUDY_SECONDS * 1000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 视频课件需要周期性检查播放状态:启动播放、设置倍速、播完返回。
|
||||||
|
let videoMissingNotified = false;
|
||||||
const videoInterval = setInterval(() => {
|
const videoInterval = setInterval(() => {
|
||||||
const video = document.querySelector(VIDEO_SELECTOR);
|
const video = document.querySelector(VIDEO_SELECTOR);
|
||||||
|
|
||||||
|
// 有些播放器会重建 video 元素,短暂找不到时继续等下一轮。
|
||||||
if (!video) {
|
if (!video) {
|
||||||
console.log("视频元素暂时不可用,继续等待");
|
if (!videoMissingNotified) {
|
||||||
|
notify("视频元素暂时不可用,继续等待", "warning");
|
||||||
|
videoMissingNotified = true;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
videoMissingNotified = false;
|
||||||
|
|
||||||
if (video.paused && video.currentTime === 0) {
|
if (video.paused && video.currentTime === 0) {
|
||||||
video.play();
|
video.play();
|
||||||
console.log("视频未播放,开始播放视频");
|
notify("视频未播放,开始播放视频", "success");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (video.currentTime >= video.duration - 1) {
|
if (video.currentTime >= video.duration - 1) {
|
||||||
console.log("视频播放结束,返回准备页面");
|
notify("视频播放结束,返回准备页面", "success");
|
||||||
clearInterval(videoInterval);
|
clearInterval(videoInterval);
|
||||||
returnToStudyPage();
|
returnToStudyPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (video.playbackRate === 1) {
|
if (video.playbackRate === 1) {
|
||||||
console.log("将视频倍速设置为2倍速");
|
notify("将视频倍速设置为2倍速", "success");
|
||||||
video.playbackRate = 2;
|
video.playbackRate = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue