GESP试卷下载

Adds a download button for question files on GESP website and handles the file downloading using JSZip library.

// ==UserScript==
// @name         GESP试卷下载
// @namespace    your-namespace
// @version      1.4
// @description  Adds a download button for question files on GESP website and handles the file downloading using JSZip library.
// @license      AGPL-3.0-or-later
// @author       Y.V
// @match        https://gesp.ccf.org.cn/*
// @grant        GM_download
// @grant        GM_addStyle
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.6.0/jszip.min.js
// @icon         https://gesp.ccf.org.cn/101/images/logo20231.png
// ==/UserScript==

(function() {
    'use strict';

    class DownloadManager {
        constructor() {
            this.progressBar = null;
            this.progressContainer = null;
            this.progressText = null;
            this.downloadButton = null;
            this.statusMessage = null;

            this.zipFilename = '';
            this.questionLinks = [];
            this.progress = 0;
            this.increment = 0;

            // 支持的文件类型映射
            this.mimeTypes = {
                'pdf': 'application/pdf',
                'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
                'doc': 'application/msword',
                'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
                'ppt': 'application/vnd.ms-powerpoint',
                'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
                'xls': 'application/vnd.ms-excel',
                'jpg': 'image/jpeg',
                'jpeg': 'image/jpeg',
                'png': 'image/png',
                'txt': 'text/plain'
            };
        }

        initialize() {
            const titleElement = document.querySelector('.title');
            this.zipFilename = titleElement ? (titleElement.textContent.trim() || 'questions') + '合集' : '试卷合集';

            this.findQuestionLinks();

            if (this.questionLinks.length > 0) {
                this.createDownloadButton();
            } else {
                console.log('没有找到可下载的试卷链接');
            }
        }

        findQuestionLinks() {
            const detailsDiv = document.querySelector('.detailsP');

            if (detailsDiv) {
                const links = detailsDiv.querySelectorAll('a');
                links.forEach((link) => {
                    const title = link.textContent.trim();
                    const url = link.href;
                    // 只添加有效的链接
                    if (title && url && url.includes('.')) {
                        this.questionLinks.push({ title, url });
                    }
                });
            }
        }

        createProgressBar() {
            this.progressContainer = document.createElement('div');
            this.progressContainer.classList.add('progress-bar-container');

            const progressInner = document.createElement('div');
            progressInner.classList.add('progress-inner');

            this.progressBar = document.createElement('div');
            this.progressBar.classList.add('progress-bar');

            this.progressText = document.createElement('div');
            this.progressText.classList.add('progress-text');
            this.progressText.textContent = '0%';

            this.statusMessage = document.createElement('div');
            this.statusMessage.classList.add('status-message');
            this.statusMessage.textContent = '准备下载...';

            progressInner.appendChild(this.progressBar);
            this.progressContainer.appendChild(progressInner);
            this.progressContainer.appendChild(this.progressText);
            this.progressContainer.appendChild(this.statusMessage);

            document.body.appendChild(this.progressContainer);
        }

        createDownloadButton() {
            this.downloadButton = document.createElement('button');
            this.downloadButton.textContent = '下载试卷';
            this.downloadButton.classList.add('download-button');
            this.downloadButton.addEventListener('click', () => this.handleDownload());

            document.body.appendChild(this.downloadButton);
        }

        handleDownload() {
            if (this.questionLinks.length === 0) {
                this.showNotification('没有找到可下载的文件', 'error');
                return;
            }

            this.downloadButton.disabled = true;
            this.downloadButton.textContent = '下载中...';
            this.createZipArchive();
        }

        async createZipArchive() {
            const JSZip = window.JSZip;
            const zip = new JSZip();

            this.increment = 100 / this.questionLinks.length;
            this.progress = 0;
            this.createProgressBar();

            let successCount = 0;
            let failCount = 0;

            for (let i = 0; i < this.questionLinks.length; i++) {
                const question = this.questionLinks[i];
                try {
                    this.updateStatusMessage(`下载文件 ${i+1}/${this.questionLinks.length}: ${question.title}`);

                    const response = await fetch(question.url);

                    if (!response.ok) {
                        throw new Error(`HTTP error! status: ${response.status}`);
                    }

                    const extension = question.url.split('.').pop().toLowerCase();
                    const arrayBuffer = await response.arrayBuffer();

                    // 获取MIME类型
                    const mimeType = this.mimeTypes[extension] || 'application/octet-stream';

                    // 确保文件名不包含非法字符
                    const safeTitle = this.sanitizeFilename(question.title);
                    const fileName = `${safeTitle}.${extension}`;

                    const fileBlob = new Blob([arrayBuffer], { type: mimeType });
                    zip.file(fileName, fileBlob);

                    successCount++;
                } catch (error) {
                    console.error(`下载文件失败: ${question.title}`, error);
                    failCount++;
                } finally {
                    this.progress += this.increment;
                    this.updateProgressBar(this.progress);
                }
            }

            try {
                this.updateStatusMessage('正在生成压缩包...');
                const zipBlob = await zip.generateAsync({
                    type: 'blob',
                    compression: 'DEFLATE',
                    compressionOptions: { level: 6 }
                });

                this.updateStatusMessage('下载完成!');
                const zipUrl = URL.createObjectURL(zipBlob);
                this.downloadFile(zipUrl, this.zipFilename + '.zip');
                URL.revokeObjectURL(zipUrl);

                const message = `下载完成! 成功: ${successCount}, 失败: ${failCount}`;
                this.showNotification(message, failCount > 0 ? 'warning' : 'success');
            } catch (error) {
                console.error('生成压缩包失败:', error);
                this.showNotification('生成压缩包失败', 'error');
            } finally {
                setTimeout(() => {
                    this.removeProgressBar();
                    this.downloadButton.disabled = false;
                    this.downloadButton.textContent = '下载试卷';
                }, 1500);
            }
        }

        downloadFile(url, fileName) {
            const link = document.createElement('a');
            link.href = url;
            link.download = fileName;
            link.style.display = 'none';
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        }

        updateProgressBar(value) {
            const percent = Math.min(Math.round(value), 100);
            this.progressBar.style.width = percent + '%';
            this.progressText.textContent = percent + '%';
        }

        updateStatusMessage(message) {
            if (this.statusMessage) {
                this.statusMessage.textContent = message;
            }
        }

        removeProgressBar() {
            if (this.progressContainer) {
                this.progressContainer.remove();
                this.progressContainer = null;
            }
        }

        showNotification(message, type = 'info') {
            const notification = document.createElement('div');
            notification.classList.add('notification', `notification-${type}`);
            notification.textContent = message;

            document.body.appendChild(notification);

            setTimeout(() => {
                notification.classList.add('show');

                setTimeout(() => {
                    notification.classList.remove('show');
                    setTimeout(() => notification.remove(), 300);
                }, 3000);
            }, 10);
        }

        sanitizeFilename(filename) {
            // 移除文件名中的非法字符
            return filename.replace(/[\\/:*?"<>|]/g, '_');
        }
    }

    // 创建DownloadManager实例并初始化
    const downloadManager = new DownloadManager();
    downloadManager.initialize();

    // 使用GM_addStyle添加CSS样式
    GM_addStyle(`
    .download-button {
        position: fixed;
        right: 20px;
        bottom: 20px;
        padding: 10px 20px;
        border: none;
        border-radius: 5px;
        background: linear-gradient(45deg, #00C6FF, #0072FF);
        color: #FFFFFF;
        font-family: Arial, sans-serif;
        font-size: 16px;
        cursor: pointer;
        box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.2);
        transition: all 0.3s ease;
        z-index: 9999;
    }

    .download-button:hover {
        transform: translateY(-2px);
        box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.3);
    }

    .download-button:disabled {
        background: linear-gradient(45deg, #B0B0B0, #808080);
        cursor: not-allowed;
        transform: none;
    }

    .progress-bar-container {
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        width: 250px;
        background-color: #FFFFFF;
        border-radius: 10px;
        box-shadow: 0px 5px 20px rgba(0, 0, 0, 0.3);
        padding: 20px;
        text-align: center;
        z-index: 10000;
    }

    .progress-inner {
        width: 100%;
        height: 10px;
        background-color: #F3F3F3;
        border-radius: 5px;
        overflow: hidden;
        margin-bottom: 10px;
    }

    .progress-bar {
        width: 0;
        height: 100%;
        background: linear-gradient(90deg, #00C6FF, #0072FF);
        transition: width 0.3s ease-in-out;
    }

    .progress-text {
        font-size: 16px;
        font-weight: bold;
        color: #333;
        margin-bottom: 10px;
    }

    .status-message {
        font-size: 14px;
        color: #666;
        margin-top: 10px;
        min-height: 20px;
    }

    .notification {
        position: fixed;
        top: 20px;
        right: 20px;
        padding: 15px 20px;
        border-radius: 5px;
        color: white;
        font-family: Arial, sans-serif;
        box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.2);
        transform: translateX(120%);
        transition: transform 0.3s ease;
        z-index: 10001;
    }

    .notification.show {
        transform: translateX(0);
    }

    .notification-success {
        background-color: #4CAF50;
    }

    .notification-error {
        background-color: #F44336;
    }

    .notification-warning {
        background-color: #FF9800;
    }

    .notification-info {
        background-color: #2196F3;
    }
`);
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址