// ==UserScript==
// @name 职教云下一节辅助工具
// @namespace https://icve.com.cn/
// @version 1.0.0
// @description 视频结束提醒、返回课程列表、定位下一节未完成课件
// @match *://*.icve.com.cn/*
// @match *://icve.com.cn/*
// @match *://*.zjy2.icve.com.cn/*
// @run-at document-idle
// @grant none
// ==/UserScript==
(() => {
'use strict';
if (window.top !== window.self) return;
if (window.__zhijiaoHelperInstalled) return;
window.__zhijiaoHelperInstalled = true;
const PREFIX = '[职教云辅助]';
const log = (...args) => console.log(PREFIX, ...args);
const visible = el =>
!!el &&
el.isConnected &&
el.getClientRects().length > 0;
const textOf = el =>
(el?.innerText || el?.textContent || '').trim();
// 查找页面中可见的按钮或链接
function findVisibleControl(pattern) {
return [...document.querySelectorAll(
'button, a, [role="button"]'
)].find(el =>
visible(el) &&
pattern.test(textOf(el) ||
el.getAttribute('aria-label') || '')
);
}
// 创建辅助面板
const panel = document.createElement('div');
panel.style.cssText = `
position:fixed;
right:18px;
bottom:20px;
z-index:2147483647;
width:250px;
padding:14px;
background:#fff;
color:#25264b;
border:1px solid #8174ff;
border-radius:12px;
box-shadow:0 5px 20px #0003;
font:14px/1.7 Arial,sans-serif;
`;
panel.innerHTML = `
职教云学习辅助
正在识别页面……
`;
document.body.appendChild(panel);
const status = panel.querySelector('#zj-status');
const result = panel.querySelector('#zj-result');
// 尝试识别课程列表中尚未完成的课件
function findIncompleteLesson() {
const progressNodes = [
...document.querySelectorAll('body *')
].filter(el => {
if (!visible(el)) return false;
if (el.children.length > 2) return false;
const t = textOf(el);
return /学习进度\s*[::]\s*\d+(?:\.\d+)?\s*%/.test(t);
});
const lessons = [];
const seen = new Set();
for (const node of progressNodes) {
const match = textOf(node).match(
/学习进度\s*[::]\s*(\d+(?:\.\d+)?)\s*%/
);
if (!match) continue;
const progress = Number(match[1]);
if (progress >= 100) continue;
// 向上寻找同时包含进度与“查看”按钮的最小容器
let container = node;
for (let i = 0; i < 8 && container; i++) {
const controls = [
...container.querySelectorAll(
'button, a, [role="button"]'
)
];
const hasView = controls.some(el =>
visible(el) && /查看/.test(textOf(el))
);
const hasProgress = /学习进度/.test(textOf(container));
if (hasView && hasProgress) break;
container = container.parentElement;
}
if (!container || seen.has(container)) continue;
const viewButton = [
...container.querySelectorAll(
'button, a, [role="button"]'
)
].find(el =>
visible(el) && /查看/.test(textOf(el))
);
if (!viewButton) continue;
seen.add(container);
lessons.push({
container,
viewButton,
progress,
title: textOf(container).slice(0, 100)
});
}
// 按页面从上到下排列
lessons.sort((a, b) =>
a.container.getBoundingClientRect().top -
b.container.getBoundingClientRect().top
);
return lessons[0] || null;
}
// 在列表页定位下一节未完成的课件
function locateNext() {
const lesson = findIncompleteLesson();
if (!lesson) {
status.textContent = '未找到可识别的未完成课件';
result.textContent =
'请确认当前是课程列表页,并且进度文字为“学习进度:xx%”。';
return;
}
lesson.container.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
lesson.container.style.outline = '3px solid #8877ff';
lesson.container.style.outlineOffset = '3px';
status.textContent =
`找到下一节候选课件:${lesson.progress}%`;
result.textContent =
'已定位到未完成课件。请核对课件名称,再点击该课件右侧的“查看”。';
log('候选课件', lesson.title, lesson.progress + '%');
}
// 返回课程活动列表
function returnToList() {
const button = findVisibleControl(/返回课堂活动列表/);
if (button) {
button.click();
result.textContent = '已尝试返回课程活动列表。';
return;
}
result.textContent =
'当前页面未找到“返回课堂活动列表”按钮,请手动返回列表。';
}
panel.querySelector('#zj-locate')
.addEventListener('click', locateNext);
panel.querySelector('#zj-return')
.addEventListener('click', returnToList);
// 检查当前页面是否有视频
const videos = [...document.querySelectorAll('video')];
if (videos.length) {
status.textContent = '已检测到视频播放器';
videos.forEach(video => {
video.addEventListener('ended', () => {
status.textContent = '视频播放结束';
result.textContent =
'请确认学习记录已保存,再点击“返回列表”,定位下一节。';
log('检测到视频 ended 事件');
});
});
} else {
status.textContent = '辅助面板已启动';
}
log('初始化完成', location.href);
})();