// ==UserScript== // @name 智慧教材极速下载 // @namespace https://scriptcat.org/zh-CN/users/211428 // @version 1.0.0 // @description 一键下载国家中小学智慧教育平台教材 // @author ganjueqi // @match https://basic.smartedu.cn/*?*contentId=* // @match https://www.smartedu.cn/*?*contentId=* // @match https://teacher.vocational.smartedu.cn/*?*contentId=* // @match https://core.teacher.vocational.smartedu.cn/*?*contentId=* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_openInTab // @connect s-file-2.ykt.cbern.com.cn // @run-at document-end // @tag 教材下载 下载 // @icon data:image/svg+xml,📘 // @license GPL-3.0 // ==/UserScript== (function () { 'use strict'; // ---------- 样式(AI 风格,增加 cursor: grab) ---------- const style = document.createElement('style'); style.textContent = ` @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } @keyframes fadeSlideIn { 0% { opacity: 0; transform: translateY(20px) scale(0.9); } 100% { opacity: 1; transform: translateY(0) scale(1); } } @keyframes fadeSlideOut { 0% { opacity: 1; transform: translateY(0) scale(1); } 100% { opacity: 0; transform: translateY(10px) scale(0.9); } } @keyframes pulseGlow { 0%, 100% { box-shadow: 0 0 20px rgba(71, 118, 230, 0.5), 0 0 60px rgba(142, 84, 233, 0.3); } 50% { box-shadow: 0 0 40px rgba(71, 118, 230, 0.8), 0 0 80px rgba(142, 84, 233, 0.5); } } .edu-download-btn { position: fixed; bottom: 30px; left: 30px; z-index: 9999; width: 80px; height: 80px; border-radius: 50%; background: linear-gradient(135deg, #4776E6, #8E54E9); animation: pulseGlow 3s ease-in-out infinite; cursor: grab; touch-action: none; display: flex; flex-direction: column; align-items: center; justify-content: center; transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s ease; user-select: none; border: none; outline: none; color: #fff; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px); } .edu-download-btn::before { content: ''; position: absolute; inset: 3px; border-radius: 50%; background: rgba(255, 255, 255, 0.08); pointer-events: none; } .edu-download-btn:hover { transform: scale(1.10); animation: none; box-shadow: 0 0 40px rgba(71, 118, 230, 0.9), 0 0 80px rgba(142, 84, 233, 0.6); } .edu-download-btn:active { transform: scale(0.92); cursor: grabbing; } .edu-download-btn .icon { font-size: 24px; line-height: 1; transition: transform 0.3s; position: relative; z-index: 1; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2)); } .edu-download-btn .label { font-size: 14px; font-weight: 700; letter-spacing: 0.8px; margin-top: 2px; position: relative; z-index: 1; text-shadow: 0 2px 8px rgba(0,0,0,0.25); } .edu-download-btn.loading .icon { animation: spin 1s linear infinite; } .edu-download-btn.loading { animation: none; } .edu-toast { position: fixed; bottom: 130px; left: 30px; background: rgba(20, 22, 36, 0.92); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); color: #fff; padding: 12px 24px; border-radius: 16px; font-size: 14px; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; box-shadow: 0 8px 32px rgba(0,0,0,0.35); z-index: 10000; animation: fadeSlideIn 0.3s ease; max-width: 320px; text-align: center; pointer-events: none; display: flex; align-items: center; gap: 10px; border: 1px solid rgba(255,255,255,0.06); } .edu-toast.error { background: rgba(60, 20, 30, 0.92); border-color: rgba(255, 80, 80, 0.2); } .edu-toast.success { background: rgba(20, 50, 40, 0.92); border-color: rgba(80, 255, 150, 0.2); } .edu-toast.hide { animation: fadeSlideOut 0.3s ease forwards; } `; document.head.appendChild(style); // ---------- 工具函数 ---------- function getContentId() { const params = new URLSearchParams(window.location.search); const id = params.get('contentId'); if (!id || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) { return null; } return id; } async function fetchAccessToken() { const MAX_RETRIES = 3; const DELAY = 500; for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { const keys = Object.keys(localStorage).filter(k => k.includes('ND_UC_AUTH') || k.includes('nd_uc_auth') ); if (keys.length === 0) { await sleep(DELAY); continue; } for (const key of keys) { const raw = localStorage.getItem(key); if (!raw) continue; let data; try { data = JSON.parse(raw); } catch { continue; } if (!data.value) continue; let parsed; try { parsed = JSON.parse(data.value); } catch { parsed = data.value; } if (parsed?.access_token) { return parsed.access_token; } } await sleep(DELAY); } return null; } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } function showToast(message, type = 'info', duration = 3000) { const existing = document.querySelector('.edu-toast'); if (existing) { existing.classList.add('hide'); setTimeout(() => existing.remove(), 350); } const toast = document.createElement('div'); toast.className = `edu-toast ${type}`; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => { toast.classList.add('hide'); setTimeout(() => toast.remove(), 350); }, duration); } // ---------- 核心下载 ---------- async function handleDownload(btn) { if (btn.classList.contains('loading')) return; btn.classList.add('loading'); try { const contentId = getContentId(); if (!contentId) { showToast('当前页面没有有效的教材ID', 'error'); btn.classList.remove('loading'); return; } const token = await fetchAccessToken(); if (!token) { showToast('未获取到登录令牌,请刷新页面重试', 'error'); btn.classList.remove('loading'); return; } const url = `https://s-file-2.ykt.cbern.com.cn/zxx/ndrv2/resources/tch_material/details/${contentId}.json`; const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, onload: resolve, onerror: reject, ontimeout: () => reject(new Error('请求超时')) }); }); if (response.status !== 200) { throw new Error(`API请求失败 (${response.status})`); } const data = JSON.parse(response.responseText); const source = data.ti_items?.find(item => item.ti_file_flag === 'source'); if (!source || !source.ti_storages?.length) { throw new Error('未找到教材存储信息'); } const parts = source.ti_storages.flatMap(s => s.split('`').map(p => p.replace(/\s+/g, ' ').trim()).filter(Boolean) ); let current = ''; const candidates = []; for (const p of parts) { if (p.startsWith('http')) { if (current) candidates.push(current); current = p; } else { current += p; } } if (current) candidates.push(current); const pdfUrl = candidates.find(u => u.toLowerCase().endsWith('.pdf')); if (!pdfUrl) { throw new Error('未找到PDF下载链接'); } const downloadUrl = `${pdfUrl}?accessToken=${token}`; GM_openInTab(downloadUrl, { active: true }); showToast('✅ 下载已在新标签页开始', 'success'); } catch (err) { console.error('下载失败:', err); showToast(`❌ ${err.message || '未知错误'}`, 'error'); } finally { btn.classList.remove('loading'); } } // ---------- 创建可拖拽按钮(使用 pointer 事件,修复错位) ---------- function createDraggableButton() { const btn = document.createElement('button'); btn.className = 'edu-download-btn'; btn.setAttribute('title', '点击下载教材 (快捷键 M)'); btn.innerHTML = ` 📘 教材下载 `; document.body.appendChild(btn); // 恢复保存的位置 const savedPos = GM_getValue('eduBtnPos'); if (savedPos) { btn.style.left = savedPos.left || '30px'; btn.style.top = savedPos.top || '30px'; btn.style.bottom = ''; btn.style.right = ''; } let startX = 0, startY = 0; let offsetX = 0, offsetY = 0; // 指针相对于按钮左上角的偏移 let isDragging = false; let wasDragged = false; const DRAG_THRESHOLD = 5; // ---------- pointer 事件 ---------- function onPointerDown(e) { // 只响应鼠标左键或触摸(pointer 自动处理) if (e.button !== undefined && e.button !== 0) return; const rect = btn.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; startX = e.clientX; startY = e.clientY; wasDragged = false; isDragging = true; btn.style.cursor = 'grabbing'; btn.setPointerCapture(e.pointerId); e.preventDefault(); } function onPointerMove(e) { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; if (!wasDragged && Math.sqrt(dx*dx + dy*dy) > DRAG_THRESHOLD) { wasDragged = true; } if (wasDragged) { let newLeft = e.clientX - offsetX; let newTop = e.clientY - offsetY; // 边界限制 const maxX = window.innerWidth - btn.offsetWidth; const maxY = window.innerHeight - btn.offsetHeight; newLeft = Math.max(0, Math.min(newLeft, maxX)); newTop = Math.max(0, Math.min(newTop, maxY)); btn.style.left = newLeft + 'px'; btn.style.top = newTop + 'px'; btn.style.bottom = ''; btn.style.right = ''; } } function onPointerUp(e) { if (!isDragging) return; isDragging = false; btn.style.cursor = 'grab'; // 判断是拖拽还是点击 if (wasDragged) { // 保存位置 GM_setValue('eduBtnPos', { left: btn.style.left, top: btn.style.top }); } else { // 点击触发下载 handleDownload(btn); } wasDragged = false; } function onPointerCancel(e) { if (isDragging) { isDragging = false; btn.style.cursor = 'grab'; wasDragged = false; } } // 注册事件 btn.addEventListener('pointerdown', onPointerDown); document.addEventListener('pointermove', onPointerMove); document.addEventListener('pointerup', onPointerUp); document.addEventListener('pointercancel', onPointerCancel); // ---------- 键盘快捷键 ---------- document.addEventListener('keydown', (e) => { if (e.key.toLowerCase() === 'm' && !['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName) && !e.target.isContentEditable) { e.preventDefault(); handleDownload(btn); } }); // 初始光标 btn.style.cursor = 'grab'; // 欢迎提示 setTimeout(() => { showToast('🤖 点击“教材下载”按钮获取教材(可拖拽)', 'info', 2800); }, 1000); return btn; } createDraggableButton(); })();