Cmpedu Resource Downloader

机械工业出版社教育服务网资源下载,无需登录,无需教师权限,油猴脚本。

// ==UserScript==
// @name         Cmpedu Resource Downloader
// @namespace    http://tampermonkey.net/
// @version      3.0
// @description  机械工业出版社教育服务网资源下载,无需登录,无需教师权限,油猴脚本。
// @author       yanyaoli
// @match        *://*.cmpedu.com/ziyuans/ziyuan/*
// @match        *://*.cmpedu.com/books/book/*
// @connect      *.cmpedu.com
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// ==/UserScript==

(function () {
  'use strict';

  // 样式注入
  GM_addStyle(`
        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');

        :root {
            --panel-bg: rgba(250, 250, 250, 0.95);
            --panel-border: rgba(0, 0, 0, 0.06);
            --header-bg: rgba(255, 255, 255, 0.8);
            --text-primary: #000000;
            --text-secondary: #6e6e73;
            --accent-color: #0071e3;
            --accent-hover: #0077ed;
            --item-hover: rgba(0, 0, 0, 0.03);
            --item-active: rgba(0, 0, 0, 0.05);
            --item-border: rgba(0, 0, 0, 0.06);
            --error-color: #ff3b30;
            --success-color: #34c759;
        }

        @media (prefers-color-scheme: dark) {
            :root {
                --panel-bg: rgba(40, 40, 40, 0.95);
                --panel-border: rgba(255, 255, 255, 0.1);
                --header-bg: rgba(50, 50, 50, 0.8);
                --text-primary: #ffffff;
                --text-secondary: #a1a1a6;
                --item-hover: rgba(255, 255, 255, 0.05);
                --item-active: rgba(255, 255, 255, 0.1);
                --item-border: rgba(255, 255, 255, 0.1);
            }
        }

        .cmp-panel {
            position: fixed;
            top: 20px;
            right: 20px;
            width: 320px;
            max-height: 75vh;
            background: var(--panel-bg);
            border-radius: 16px;
            box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12), 0 2px 6px rgba(0, 0, 0, 0.08);
            backdrop-filter: blur(20px);
            -webkit-backdrop-filter: blur(20px);
            z-index: 99999;
            font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
            overflow: hidden;
            transition: left 0.1s ease-out, top 0.1s ease-out;
            border: 1px solid var(--panel-border);
            transform-origin: top right;
            opacity: 0;
            transform: scale(0.95);
            animation: panel-appear 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
        }

        @keyframes panel-appear {
            0% { opacity: 0; transform: scale(0.95); }
            100% { opacity: 1; transform: scale(1); }
        }

        .panel-header {
            padding: 16px 20px;
            background: var(--header-bg);
            backdrop-filter: blur(10px);
            -webkit-backdrop-filter: blur(10px);
            border-bottom: 1px solid var(--item-border);
            display: flex;
            justify-content: space-between;
            align-items: center;
            position: sticky;
            top: 0;
            z-index: 1;
        }

        .panel-title {
            margin: 0;
            font-size: 16px;
            color: var(--text-primary);
            font-weight: 600;
            letter-spacing: -0.01em;
        }

        .close-btn {
            background: none;
            border: none;
            cursor: pointer;
            color: var(--text-secondary);
            font-size: 22px;
            line-height: 1;
            padding: 4px;
            border-radius: 50%;
            width: 28px;
            height: 28px;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all 0.2s ease;
            margin: -4px;
        }

        .close-btn:hover {
            background-color: var(--item-hover);
            color: var(--text-primary);
        }

        .close-btn:active {
            background-color: var(--item-active);
            transform: scale(0.95);
        }

        .panel-content {
            padding: 8px 0;
            max-height: calc(75vh - 60px);
            overflow-y: auto;
            overflow-x: hidden;
            scrollbar-width: thin;
            scrollbar-color: var(--text-secondary) transparent;
        }

        .panel-content::-webkit-scrollbar {
            width: 6px;
        }

        .panel-content::-webkit-scrollbar-track {
            background: transparent;
        }

        .panel-content::-webkit-scrollbar-thumb {
            background-color: var(--text-secondary);
            border-radius: 3px;
            opacity: 0.5;
        }

        .resource-item {
            padding: 12px 20px;
            display: flex;
            align-items: center;
            justify-content: space-between;
            border-bottom: 1px solid var(--item-border);
            cursor: pointer;
            transition: all 0.15s ease;
            position: relative;
            color: var(--text-primary);
        }

        .resource-item:last-child {
            border-bottom: none;
        }

        .resource-item:hover {
            background-color: var(--item-hover);
        }

        .resource-item:active {
            background-color: var(--item-active);
        }

        .resource-item.disabled {
            opacity: 0.6;
            cursor: not-allowed;
        }

        .resource-item .title {
            flex: 1;
            font-weight: 500;
            font-size: 14px;
            margin-right: 12px;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }

        .resource-item .download-icon {
            color: var(--accent-color);
            font-size: 16px;
            transition: color 0.2s ease;
            margin-left: 5px;
        }

        .resource-item .download-icon:hover {
            color: var(--accent-hover);
        }

        .skeleton {
            height: 20px;
            margin: 12px 20px;
            border-radius: 6px;
            background: linear-gradient(90deg,
                var(--item-border) 0%,
                var(--item-hover) 50%,
                var(--item-border) 100%);
            background-size: 200% 100%;
            animation: skeleton-loading 1.5s infinite;
        }

        .skeleton:nth-child(2) {
            width: 85%;
        }

        .skeleton:nth-child(3) {
            width: 70%;
        }

        @keyframes skeleton-loading {
            0% { background-position: 200% 0; }
            100% { background-position: -200% 0; }
        }

        .empty-state {
            padding: 40px 20px;
            text-align: center;
            color: var(--text-secondary);
            font-size: 14px;
        }

        .error-message {
            color: var(--error-color);
            display: flex;
            align-items: center;
            gap: 8px;
            padding: 12px 20px;
            background: rgba(255, 59, 48, 0.1);
            border-radius: 8px;
            margin: 12px 20px;
            font-size: 14px;
        }

        .success-message {
            color: var(--success-color);
            display: flex;
            align-items: center;
            gap: 8px;
            padding: 12px 20px;
            background: rgba(52, 199, 89, 0.1);
            border-radius: 8px;
            margin: 12px 20px;
            font-size: 14px;
        }

        .github-icon {
            width: 16px;
            height: 16px;
            cursor: pointer;
            transition: transform 0.2s ease;
        }

        .github-icon:hover {
            transform: scale(1.1);
        }

        .panel-footer {
            padding: 12px 20px;
            border-top: 1px solid var(--item-border);
            display: flex;
            justify-content: space-between;
            align-items: center;
            font-size: 12px;
            color: var(--text-secondary);
        }

        @media (max-width: 480px) {
            .cmp-panel {
                width: calc(100% - 32px);
                right: 16px;
                left: 16px;
                top: 16px;
                max-height: 80vh;
            }
        }
    `);

  // 基础配置
  const isMobile = window.location.host.startsWith('m.');
  const baseUrl = isMobile ? 'http://m.cmpedu.com' : 'http://www.cmpedu.com';
  const panelId = "downloadPanel";

  function extractBookId() {
    if (window.location.href.includes('books/book')) {
      return window.location.pathname.split("/").pop().split(".")[0];
    }
    if (window.location.href.includes('ziyuans/ziyuan')) {
      const el = document.getElementById('BOOK_ID');
      return el ? el.value : null;
    }
    return null;
  }

  function createPanel() {
    const panel = document.createElement('div');
    panel.id = panelId;
    panel.className = 'cmp-panel';
    panel.innerHTML = `
            <div class="panel-header">
                <h3 class="panel-title">机工教育资源下载</h3>
                <button class="close-btn" aria-label="关闭">×</button>
            </div>
            <div class="panel-content">
                <div class="skeleton"></div>
                <div class="skeleton"></div>
                <div class="skeleton"></div>
            </div>
            <div class="panel-footer">
                <span>v3.1</span>
                <a href="https://github.com/yanyaoli/cmpedu-dl" target="_blank">
                    <img src="https://simpleicons.org/icons/github.svg" alt="GitHub" class="github-icon" />
                </a>
            </div>
        `;
    document.body.appendChild(panel);

    // 添加关闭动画
    panel.querySelector('.close-btn').addEventListener('click', () => {
      panel.style.opacity = '0';
      panel.style.transform = 'scale(0.95)';
      setTimeout(() => panel.remove(), 300);
    });

    // 拖动面板
    const header = panel.querySelector('.panel-header');
    let isDragging = false;
    let offset = { x: 0, y: 0 };

    header.addEventListener('mousedown', (e) => {
      isDragging = true;
      offset.x = e.clientX - panel.getBoundingClientRect().left;
      offset.y = e.clientY - panel.getBoundingClientRect().top;
      document.body.style.cursor = 'grabbing'; // 改成抓取手势
      e.preventDefault(); // 防止文本选择
    });

    document.addEventListener('mousemove', (e) => {
      if (!isDragging) return;

      // 确保面板的移动使用 `requestAnimationFrame`
      window.requestAnimationFrame(() => {
        panel.style.left = `${e.clientX - offset.x}px`;
        panel.style.top = `${e.clientY - offset.y}px`;
      });
    });

    document.addEventListener('mouseup', () => {
      isDragging = false;
      document.body.style.cursor = 'default'; // 恢复光标
    });

    return panel;
  }

  function updatePanelContent(panel, content) {
    const panelContent = panel.querySelector('.panel-content');
    panelContent.innerHTML = content;
  }

  function createResourceItem(title, index) {
    return `
            <div class="resource-item" data-index="${index}">
                <div class="title">${title}</div>
                <svg class="download-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
                    <polyline points="7 10 12 15 17 10"></polyline>
                    <line x1="12" y1="15" x2="12" y2="3"></line>
                </svg>
            </div>
        `;
  }

  function updateResourceItem(panel, index, content, downloadLink) {
    const items = panel.querySelectorAll('.resource-item');
    let item = null;

    // 查找对应索引的项目
    for (let i = 0; i < items.length; i++) {
      if (items[i].getAttribute('data-index') == index) {
        item = items[i];
        break;
      }
    }

    if (item) {
      if (downloadLink) {
        item.innerHTML = `
                    <div class="title">${content}</div>
                    <svg class="download-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
                        <polyline points="7 10 12 15 17 10"></polyline>
                        <line x1="12" y1="15" x2="12" y2="3"></line>
                    </svg>
                `;
        item.style.cursor = 'pointer';
        item.classList.remove('disabled');
        item.setAttribute('data-download-link', downloadLink);

        // 添加点击效果
        item.onclick = () => {
          window.open(downloadLink, '_blank');
        };
      } else {
        item.innerHTML = `
                    <div class="title">${content}</div>
                    <span style="font-size: 12px; color: var(--error-color);">无法下载</span>
                `;
        item.classList.add('disabled');
        item.style.cursor = 'not-allowed';
      }
    } else {
      // 如果没有找到现有项,添加新项
      const panelContent = panel.querySelector('.panel-content');
      const newItem = document.createElement('div');
      newItem.className = 'resource-item';
      newItem.setAttribute('data-index', index);

      if (downloadLink) {
        newItem.innerHTML = `
                    <div class="title">${content}</div>
                    <svg class="download-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
                        <polyline points="7 10 12 15 17 10"></polyline>
                        <line x1="12" y1="15" x2="12" y2="3"></line>
                    </svg>
                `;
        newItem.style.cursor = 'pointer';
        newItem.setAttribute('data-download-link', downloadLink);
        newItem.onclick = () => {
          window.open(downloadLink, '_blank');
        };
      } else {
        newItem.innerHTML = `
                    <div class="title">${content}</div>
                    <span style="font-size: 12px; color: var(--error-color);">无法下载</span>
                `;
        newItem.classList.add('disabled');
        newItem.style.cursor = 'not-allowed';
      }

      panelContent.appendChild(newItem);
    }
  }

  function processResourceResponse(response, title) {
    const downloadLinks = response.responseText.match(/window\.location\.href=\'(https?:\/\/[^\'"]+)\'/);
    if (downloadLinks) {
      return [title, downloadLinks[1]];
    }
    return [null, null];
  }

  // 主逻辑
  const bookId = extractBookId();
  if (!bookId) {
    console.error("无法提取 BOOK_ID");
    return;
  }

  const resourceUrl = `${baseUrl}/ziyuans/index.htm?BOOK_ID=${bookId}`;
  const panel = createPanel();

  GM_xmlhttpRequest({
    method: "GET",
    url: resourceUrl,
    onload: function (response) {
      const parser = new DOMParser();
      const doc = parser.parseFromString(response.responseText, "text/html");
      const resourceDivs = doc.querySelectorAll("div.row.gjzy_list");
      const resources = Array.from(resourceDivs).map(div => {
        return {
          title: div.querySelector("div.gjzy_listRTit")?.textContent.trim() || "未知资源",
          resourceId: div.querySelector("a")?.href.split("/").pop().split(".")[0]
        };
      });

      if (resources.length === 0) {
        updatePanelContent(panel, `
                    <div class="empty-state">
                        <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
                            <circle cx="12" cy="12" r="10"></circle>
                            <line x1="12" y1="8" x2="12" y2="12"></line>
                            <line x1="12" y1="16" x2="12.01" y2="16"></line>
                        </svg>
                        <p>未找到可下载的资源</p>
                    </div>
                `);
        return;
      }

      // 清除骨架屏,添加真实内容
      updatePanelContent(panel, '');

      // 先显示所有资源项的占位
      resources.forEach(({ title }, index) => {
        const panelContent = panel.querySelector('.panel-content');
        const itemHTML = createResourceItem(title, index);
        panelContent.innerHTML += itemHTML;
      });

      // 然后逐个请求下载链接
      resources.forEach(({ title, resourceId }, index) => {
        const downloadUrl = `${baseUrl}/ziyuans/d_ziyuan.df?id=${resourceId}`;
        GM_xmlhttpRequest({
          method: "GET",
          url: downloadUrl,
          headers: {
            "Accept-Encoding": "gzip, deflate",
            "Connection": "keep-alive",
            "Accept": "text/html, */*; q=0.01",
            "User-Agent": "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5",
            "Accept-Language": "en-US,en;q=0.9",
            "X-Requested-With": "XMLHttpRequest"
          },
          onload: function (response) {
            const [resourceTitle, downloadLink] = processResourceResponse(response, title);
            if (resourceTitle) {
              updateResourceItem(panel, index, resourceTitle, downloadLink);
            } else {
              updateResourceItem(panel, index, `${title} - 链接解析失败`, null);
            }
          },
          onerror: function () {
            updateResourceItem(panel, index, `${title} - 请求失败`, null);
          }
        });
      });
    },
    onerror: function () {
      updatePanelContent(panel, `
                <div class="error-message">
                    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <circle cx="12" cy="12" r="10"></circle>
                        <line x1="12" y1="8" x2="12" y2="12"></line>
                        <line x1="12" y1="16" x2="12.01" y2="16"></line>
                    </svg>
                    <span>获取资源页面失败,请刷新重试</span>
                </div>
            `);
    }
  });
})();