// ==UserScript==
// @name 山东省继续医学教育公需科目学习平台 - 清空课程进度(免费版)
// @namespace https://github.com/
// @version 1.0
// @description 免费版本 - 仅支持清空课程进度功能。点击页面顶部悬浮按钮即可使用。
// @include *://*.sdcme.net.cn/*
// @include *://*sdcme.net.cn*
// @grant GM_addStyle
// @require https://m.zhanyc.cn/jquery-2.2.4.min.js
// @require https://m.zhanyc.cn/layerjs-gm-with-css.js
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
const FREE = {
// ======================== 清空课程进度核心方法 ========================
async clearCourseProgress() {
if (location.href.includes('/play/index.html')) {
this.alertMsg('请不要在视频页面操作该功能!');
return;
}
// 1. 引流提示
let proceed = await new Promise((resolve) => {
layer.open({
type: 1,
title: '温馨提示',
content: '
',
area: ['480px', '300px'],
btn: ['知道了,继续清理'],
yes: function(index) {
layer.close(index);
resolve(true);
},
cancel: function() {
resolve(true);
}
});
});
if (!proceed) return;
let firstIndex;
// 1. 加载弹窗
let loadingIndex = layer.open({
type: 1,
title: '清空课程进度',
content: '请等待……
',
area: ['400px', '150px'],
btn: false
});
this.tipsMsg2('正在获取课程列表...', 0);
let courses;
try {
courses = await this.getCourses();
} catch (e) {
layer.close(loadingIndex);
this.alertMsg('获取课程列表失败:' + e.message);
return;
}
layer.close(loadingIndex);
if (!courses || courses.length === 0) {
this.alertMsg('没有获取到任何课程');
return;
}
// 2. 课程选择对话框
this.tipsMsg2('正在显示课程列表...');
let selectedCourseIds = await new Promise((resolve) => {
let html = '';
html += '
';
html += '
';
html += '';
html += ' ';
html += '';
html += ' ';
html += '';
html += '
';
courses.forEach(course => {
html += `
`;
});
html += '
';
firstIndex = layer.open({
type: 1,
title: '选择需要清空进度的课程',
content: html,
area: ['500px', '500px'],
btn: ['确认清空', '取消'],
yes: function (index) {
let ids = [];
$('.zfk-clear-course-cb:checked').each(function () {
ids.push($(this).val());
});
if (ids.length === 0) {
FREE.tipsMsg('请至少选择一个课程');
return false;
}
layer.open({
type: 1,
title: '确认清空',
content: `是否确认清空选中的${ids.length}个课程的所有章节进度?
`,
btn: ['确认', '取消'],
yes: function (confirmIndex) {
layer.close(confirmIndex);
layer.close(index);
resolve(ids);
},
btn2: function (confirmIndex) {
layer.close(confirmIndex);
}
});
return false;
},
btn2: function (index) {
layer.close(index);
resolve([]);
}
});
});
if (selectedCourseIds.length === 0) return;
// 3. 获取所有选中课程的章节列表
this.tipsMsg2('正在获取章节列表...', 0);
let allChapters = [];
let selectedCourses = courses.filter(c => selectedCourseIds.includes(String(c.id)));
let chapterPromises = selectedCourses.map(async (course) => {
try {
let chapters = await this.getChapters(course.id);
chapters.forEach(ch => { ch.courseTitle = course.title; });
return chapters;
} catch (e) {
this.tipsMsg2('获取课程【' + course.title + '】章节列表失败:' + e.message);
return [];
}
});
let chaptersResults = await Promise.all(chapterPromises);
allChapters = chaptersResults.flat();
layer.closeAll();
if (allChapters.length === 0) {
this.alertMsg('没有获取到任何章节信息');
return;
}
// 4. 进度面板
this.tipsMsg2('正在清空课程进度...', 0);
let total = allChapters.length;
let successCount = 0;
let failCount = 0;
let completedCount = 0;
let groupHtml = '';
let lastCourseTitle = '';
allChapters.forEach((chapter, idx) => {
if (chapter.courseTitle !== lastCourseTitle) {
if (lastCourseTitle) groupHtml += '';
groupHtml += '' + chapter.courseTitle + '';
lastCourseTitle = chapter.courseTitle;
}
groupHtml += '
' + chapter.title + ' 等待处理...
';
});
if (lastCourseTitle) groupHtml += '
';
let panelHtml =
'' +
'
概览数据:
' +
'
' +
'
' +
'进度:0/' + total + ' | 成功:0 | 失败:0
' +
'
课程数据:
' +
'
' +
groupHtml +
'
' +
'
' +
'' +
'
';
layer.close(firstIndex);
let clearLayerIndex = layer.open({
type: 1,
title: '清空课程进度',
content: panelHtml,
area: ['620px', '650px'],
closeBtn: true,
btn: false
});
$('#zfk-clear-close-btn').on('click', function () { layer.close(clearLayerIndex); });
// 5. 并发清空
let maxConcurrency = Number($('#zfk-clear-thread-count').val()) || 5;
async function processOneChapter(chapter, idx) {
let resultMsg = '';
let isSuccess = false;
try {
let res = await FREE.resetChapter(chapter.fileName);
if (res.code === 200 && res.msg === '该视频进度重置成功,请重新进入该课件进行学习') {
successCount++;
resultMsg = '成功';
isSuccess = true;
} else {
failCount++;
resultMsg = res.msg || '失败';
}
} catch (e) {
failCount++;
resultMsg = '请求失败';
}
completedCount++;
$('#zfk-clear-progress-overview').html('进度:' + completedCount + '/' + total + ' | 成功:' + successCount + ' | 失败:' + failCount);
let color = isSuccess ? '#28a745' : '#dc3545';
$('#zfk-chapter-' + idx + ' span').html('' + resultMsg + '');
}
let chapterIndex = 0;
async function worker() {
while (chapterIndex < allChapters.length) {
let i = chapterIndex++;
await processOneChapter(allChapters[i], i);
}
}
let workers = [];
let workerCount = Math.min(maxConcurrency, allChapters.length);
for (let w = 0; w < workerCount; w++) {
workers.push(worker());
}
await Promise.all(workers);
this.tipsMsg2('清空完成:成功 ' + successCount + ' 个,失败 ' + failCount + ' 个');
},
async getCourses() {
let res = await fetch("https://course.sdcme.net.cn/courses", {
headers: {
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9",
"content-type": "application/json",
"x-requested-with": "XMLHttpRequest"
},
referrer: "https://course.sdcme.net.cn/",
method: "GET",
mode: "cors",
credentials: "include"
});
let data = await res.json();
if (data.code === 200 && data.success) {
return data.data;
}
throw new Error(data.msg || "获取课程列表失败");
},
async getChapters(courseId) {
let res = await fetch("https://course.sdcme.net.cn/coursewares/course/" + courseId, {
headers: {
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9",
"content-type": "application/json",
"x-requested-with": "XMLHttpRequest"
},
referrer: "https://course.sdcme.net.cn/course/" + courseId,
method: "GET",
mode: "cors",
credentials: "include"
});
let data = await res.json();
if (data.code === 200 && data.success) {
return data.data;
}
throw new Error(data.msg || "获取章节列表失败");
},
async resetChapter(videoId) {
let res = await fetch("https://course.sdcme.net.cn/api/rewatch", {
headers: {
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9",
"content-type": "application/json",
"x-requested-with": "XMLHttpRequest"
},
referrer: "https://course.sdcme.net.cn/",
body: JSON.stringify({ videoId: videoId }),
method: "POST",
mode: "cors",
credentials: "include"
});
return await res.json();
},
// ======================== UI 工具方法 ========================
alertMsg(msg) {
layer.open({
type: "1",
content: '' + msg + '
',
title: "提示",
offset: "100px",
btn: "关闭"
});
},
tipsMsg(msg, timeout) {
layer.msg(msg, { offset: "100px", time: timeout || 3000 });
},
tipsMsg2(msg, timeout) {
layer.msg(msg, { offset: "100px", time: timeout || 3000, icon: 0 });
},
// ======================== 初始化 ========================
init() {
// 添加悬浮按钮样式
GM_addStyle(`
#free-clear-btn {
position: fixed;
top: 10px;
left: 10px;
z-index: 999999;
padding: 12px 24px;
background: linear-gradient(135deg, #ff6b6b, #ee5a24);
color: #fff;
font-size: 16px;
font-weight: bold;
border: 2px solid #fff;
border-radius: 8px;
cursor: pointer;
box-shadow: 0 4px 15px rgba(238, 90, 36, 0.4);
transition: all 0.3s ease;
font-family: "Microsoft YaHei", sans-serif;
}
#free-clear-btn:hover {
transform: scale(1.05);
box-shadow: 0 6px 20px rgba(238, 90, 36, 0.6);
}
#free-clear-btn:active {
transform: scale(0.98);
}
`);
// 等待 DOM 就绪后注入按钮
let checkExist = setInterval(function () {
if (document.body) {
clearInterval(checkExist);
let btn = document.createElement('button');
btn.id = 'free-clear-btn';
btn.textContent = '🧹 清空课程进度';
btn.addEventListener('click', function () {
FREE.clearCourseProgress();
});
document.body.appendChild(btn);
}
}, 500);
}
};
setTimeout(() => {
debugger
if (typeof(zfk)=='undefined') {
FREE.init();
} else {
console.log('skip init');
}
}, 3000);
})();