// ==UserScript==
// @name 华医网课程自动收藏工具(批量)V2.3
// @namespace http://tampermonkey.net/
// @version 2.3
// @description 高效稳定的华医网课程收藏工具,支持批量收藏,优化了收藏速度[POST]方式,不是很完美,可能会漏掉课程~后续版本已经更新解决
// @author vx:hapens1986
// @match https://cme28.91huayi.com/*
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_openInTab
// @grant GM_notification
// @connect 91huayi.com
// ==/UserScript==
(function() {
'use strict';
GM_addStyle(`
#cid-input-container {
position: fixed;
top: 100px;
right: 20px;
z-index: 9999;
background: white;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0,0,0,0.2);
font-family: Arial, sans-serif;
width: 300px;
}
#cid-input {
width: 100%;
padding: 8px;
margin: 10px 0 5px;
border: 1px solid #ccc;
border-radius: 3px;
box-sizing: border-box;
}
#cid-input-batch {
width: 100%;
padding: 8px;
margin: 10px 0 5px;
border: 1px solid #ccc;
border-radius: 3px;
box-sizing: border-box;
height: 300px;
resize: vertical;
}
#collect-btn, #batch-collect-btn {
background: #1277af;
color: white;
border: none;
padding: 8px 15px;
border-radius: 3px;
cursor: pointer;
width: 100%;
margin: 5px 0;
transition: background 0.2s;
}
#batch-collect-btn {
background: #5cb85c;
}
#collect-btn:hover {
background: #0e6a9c;
}
#batch-collect-btn:hover {
background: #4cae4c;
}
#collect-btn:disabled, #batch-collect-btn:disabled {
background: #cccccc;
cursor: not-allowed;
}
#status-message {
margin-top: 10px;
font-size: 13px;
line-height: 1.4;
}
.success {
color: #28a745;
}
.error {
color: #dc3545;
}
.warning {
color: #f0ad4e;
}
.loading {
color: #17a2b8;
}
.course-name {
font-weight: bold;
margin: 5px 0;
}
.hidden-iframe {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
.toggle-mode-btn {
background: #6c757d;
color: white;
border: none;
padding: 6px 12px;
border-radius: 3px;
cursor: pointer;
width: 100%;
margin: 5px 0;
transition: background 0.2s;
font-size: 13px;
}
.toggle-mode-btn:hover {
background: #5a6268;
}
.batch-instruction {
font-size: 12px;
color: #666;
margin: 5px 0;
}
.progress-bar {
width: 100%;
background-color: #f3f3f3;
border-radius: 3px;
margin: 5px 0;
}
.progress-bar-fill {
height: 20px;
background-color: #4CAF50;
border-radius: 3px;
width: 0%;
transition: width 0.3s;
text-align: center;
line-height: 20px;
color: white;
font-size: 12px;
}
`);
const container = document.createElement('div');
container.id = 'cid-input-container';
container.innerHTML = `
华医网课程收藏工具
提示:可以输入多个CID,每行一个或用逗号分隔
`;
document.body.appendChild(container);
const cidInput = document.getElementById('cid-input');
const cidInputBatch = document.getElementById('cid-input-batch');
const collectBtn = document.getElementById('collect-btn');
const batchCollectBtn = document.getElementById('batch-collect-btn');
const statusMessage = document.getElementById('status-message');
const hiddenIframe = document.getElementById('hidden-iframe');
const singleMode = document.getElementById('single-mode');
const batchMode = document.getElementById('batch-mode');
const modeToggle = document.getElementById('mode-toggle');
const progressContainer = document.getElementById('progress-container');
const progressBar = document.getElementById('progress-bar');
let isBatchMode = false;
let abortController = null;
function setStatus(message, type = '') {
statusMessage.innerHTML = message;
statusMessage.className = type;
}
function updateProgress(current, total) {
const percent = Math.round((current / total) * 100);
progressBar.style.width = `${percent}%`;
progressBar.textContent = `${percent}% (${current}/${total})`;
}
function toggleMode() {
isBatchMode = !isBatchMode;
singleMode.style.display = isBatchMode ? 'none' : 'block';
batchMode.style.display = isBatchMode ? 'block' : 'none';
modeToggle.textContent = isBatchMode ? '切换到单条模式' : '切换到批量模式';
setStatus('');
}
async function getCourseInfo(cid) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", `https://cme28.91huayi.com/pages/course.aspx?cid=${cid}`, true);
xhr.timeout = 5000; // 5秒超时
xhr.onload = function() {
if (xhr.status === 200) {
try {
// 使用更高效的方式提取课程名称
const parser = new DOMParser();
const doc = parser.parseFromString(xhr.responseText, "text/html");
const titleElement = doc.querySelector('h1.course-title');
const title = titleElement ? titleElement.textContent.trim() : `课程 (CID: ${cid})`;
// 检查是否已收藏
const isCollected = xhr.responseText.includes('id="btnCollect" data-collect="1"');
resolve({ title, isCollected });
} catch (e) {
resolve({ title: `课程 (CID: ${cid})`, isCollected: false }); // 即使解析失败也继续
}
} else {
resolve({ title: `课程 (CID: ${cid})`, isCollected: false }); // 即使请求失败也继续
}
};
xhr.onerror = function() {
resolve({ title: `课程 (CID: ${cid})`, isCollected: false }); // 即使请求失败也继续
};
xhr.ontimeout = function() {
resolve({ title: `课程 (CID: ${cid})`, isCollected: false }); // 即使超时也继续
};
xhr.send();
});
}
async function collectCourse(cid, title) {
return new Promise((resolve) => {
const formData = new FormData();
formData.append('cid', cid);
formData.append('action', 'collect');
const xhr = new XMLHttpRequest();
xhr.open("POST", "https://cme28.91huayi.com/ajax/course.ashx", true);
xhr.timeout = 5000; // 5秒超时
xhr.onload = function() {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.success) {
resolve(true);
} else {
resolve(false);
}
} catch (e) {
resolve(false);
}
} else {
resolve(false);
}
};
xhr.onerror = function() {
resolve(false);
};
xhr.ontimeout = function() {
resolve(false);
};
xhr.send(formData);
});
}
async function collectSingleCourse(cid) {
try {
// 获取课程信息 - 不等待结果,直接开始收藏
const infoPromise = getCourseInfo(cid);
// 直接尝试收藏
const success = await collectCourse(cid);
// 获取课程信息结果
const { title, isCollected } = await infoPromise;
if (isCollected) {
return { success: false, message: `课程已收藏: ${title}` };
}
if (success) {
return { success: true, message: `收藏成功: ${title}` };
} else {
const iframeSuccess = await collectWithIframe(cid, title);
if (iframeSuccess) {
return { success: true, message: `收藏成功: ${title}` };
} else {
GM_openInTab(`https://cme28.91huayi.com/pages/course.aspx?cid=${cid}`, false);
return { success: false, message: `请在新标签页中手动收藏: ${title}` };
}
}
} catch (error) {
return { success: false, message: `收藏失败: ${error}` };
}
}
async function collectWithIframe(cid, title) {
return new Promise((resolve) => {
hiddenIframe.src = `https://cme28.91huayi.com/pages/course.aspx?cid=${cid}`;
let checkCount = 0;
const maxChecks = 3; // 减少检查次数
const checkInterval = 800; // 缩短检查间隔
const checkCollection = setInterval(() => {
checkCount++;
try {
const iframeDoc = hiddenIframe.contentDocument || hiddenIframe.contentWindow.document;
const collectBtn = iframeDoc.getElementById('btnCollect');
if (collectBtn) {
clearInterval(checkCollection);
if (collectBtn.getAttribute('data-collect') === '1') {
resolve(true);
} else {
collectBtn.click();
setTimeout(() => {
resolve(true);
}, 1000); // 缩短等待时间
}
} else if (checkCount >= maxChecks) {
clearInterval(checkCollection);
resolve(false);
}
} catch (e) {
if (checkCount >= maxChecks) {
clearInterval(checkCollection);
resolve(false);
}
}
}, checkInterval);
// 设置总超时
setTimeout(() => {
clearInterval(checkCollection);
resolve(false);
}, (maxChecks + 1) * checkInterval);
});
}
async function handleSingleCollect() {
const cid = cidInput.value.trim();
if (!cid) {
setStatus('请输入有效的CID值', 'error');
return;
}
collectBtn.disabled = true;
setStatus('正在处理...', 'loading');
const result = await collectSingleCourse(cid);
if (result.success) {
setStatus(`${result.message}
`, 'success');
GM_notification({
title: '收藏成功',
text: result.message,
timeout: 2000 // 缩短通知显示时间
});
} else {
setStatus(`${result.message}
`, result.message.includes('手动收藏') ? 'warning' : 'error');
}
collectBtn.disabled = false;
}
async function handleBatchCollect() {
const input = cidInputBatch.value.trim();
if (!input) {
setStatus('请输入有效的CID值', 'error');
return;
}
const cids = input.split(/[\n,]+/).map(cid => cid.trim()).filter(cid => cid);
if (cids.length === 0) {
setStatus('未检测到有效的CID值', 'error');
return;
}
abortController = new AbortController();
const abortSignal = abortController.signal;
const originalBtnText = batchCollectBtn.textContent;
batchCollectBtn.textContent = '取消批量收藏';
batchCollectBtn.onclick = cancelBatchCollect;
batchCollectBtn.disabled = false;
progressContainer.style.display = 'block';
updateProgress(0, cids.length);
setStatus(`准备收藏 ${cids.length} 个课程...`, 'loading');
let successCount = 0;
let messages = [];
let shouldAbort = false;
const concurrencyLimit = 3; // 并发数
const batches = Math.ceil(cids.length / concurrencyLimit);
for (let i = 0; i < batches; i++) {
if (shouldAbort) break;
const batchStart = i * concurrencyLimit;
const batchEnd = Math.min((i + 1) * concurrencyLimit, cids.length);
const batchCids = cids.slice(batchStart, batchEnd);
const batchResults = await Promise.all(batchCids.map(async (cid, index) => {
if (abortSignal.aborted) {
return { cid, message: '操作已取消', success: false };
}
const globalIndex = batchStart + index;
setStatus(`正在处理第 ${globalIndex+1}/${cids.length} 个课程 (CID: ${cid})...`, 'loading');
const result = await collectSingleCourse(cid);
updateProgress(globalIndex + 1, cids.length);
return { cid, ...result };
}));
for (const result of batchResults) {
if (result.success) {
successCount++;
messages.push(`✓ ${result.message}`);
} else {
messages.push(`✗ ${result.message}`);
}
}
if (i < batches - 1 && !shouldAbort) {
await new Promise(resolve => setTimeout(resolve, 300));
}
}
batchCollectBtn.textContent = originalBtnText;
batchCollectBtn.onclick = handleBatchCollect;
setStatus(`
已完成 ${cids.length} 个课程处理
成功收藏: ${successCount} 个
${shouldAbort ? '操作已取消
' : ''}
详细结果:
${messages.join('
')}
`, successCount === cids.length ? 'success' : (successCount > 0 ? 'warning' : 'error'));
batchCollectBtn.disabled = false;
if (successCount > 0 && !shouldAbort) {
GM_notification({
title: '批量收藏完成',
text: `成功收藏 ${successCount}/${cids.length} 个课程`,
timeout: 3000
});
}
}
function cancelBatchCollect() {
if (abortController) {
abortController.abort();
}
}
collectBtn.addEventListener('click', handleSingleCollect);
batchCollectBtn.addEventListener('click', handleBatchCollect);
modeToggle.addEventListener('click', toggleMode);
// 监听输入框回车键
cidInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
handleSingleCollect();
}
});
function getCIDFromURL() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('cid');
}
const currentCID = getCIDFromURL();
if (currentCID) {
cidInput.value = currentCID;
}
})();