// ==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 = `

华医网课程收藏工具

`; 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; } })();