// ==UserScript==
// @name PDFJM Downloader
// @namespace pdfjm-downloader
// @version 2025-06-23
// @description Download PDFJM Origin PDF
// @author delph1s
// @license MIT
// @match https://pdfjm.cn/api/pdf/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=pdfjm.cn
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// 全局状态管理
const state = {
pdfUrl: null,
pdfBlob: null,
downloadButton: null,
notificationContainer: null,
activeNotifications: [],
isInitialized: false
};
// 配置常量
const CONFIG = {
BUTTON_TEXT: {
WAITING: '等待PDF链接...',
LOADING: '等待PDF加载...',
READY: '立即下载',
DOWNLOADING: '下载中...'
},
NOTIFICATION_DURATION: {
SHORT: 2000,
NORMAL: 3000,
LONG: 4000,
ERROR: 5000
}
};
// CSS 样式(简化版)
const CSS_STYLES = `
.pdf-download-btn {
position: fixed; bottom: 20px; left: 20px; z-index: 10000;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; border: none; border-radius: 50px;
padding: 15px 25px; font-size: 14px; font-weight: 600;
cursor: pointer; transition: all 0.3s ease;
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
display: flex; align-items: center; gap: 8px;
min-width: 140px; justify-content: center;
}
.pdf-download-btn:hover {
transform: translateY(-2px);
box-shadow: 0 12px 35px rgba(102, 126, 234, 0.4);
}
.pdf-download-btn:disabled {
background: #a0aec0; cursor: not-allowed;
box-shadow: 0 4px 15px rgba(160, 174, 192, 0.2);
}
.notification-container {
position: fixed; top: 20px; right: 20px; z-index: 10001;
pointer-events: none; display: flex; flex-direction: column;
gap: 12px; max-width: 400px;
}
.notification {
padding: 16px 20px; border-radius: 12px; color: white;
font-weight: 500; font-size: 14px; line-height: 1.4;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
transform: translateX(100%); opacity: 0;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: auto; backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.notification.show { transform: translateX(0); opacity: 1; }
.notification.hide { transform: translateX(100%); opacity: 0; }
.notification.success { background: linear-gradient(135deg, #10b981 0%, #059669 100%); }
.notification.error { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); }
.notification.info { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); }
.notification.warning { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); }
.loading-spinner {
width: 16px; height: 16px; border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%; border-top-color: white;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (max-width: 480px) {
.notification-container { left: 20px; right: 20px; max-width: none; }
.notification { transform: translateY(-100%); }
.notification.show { transform: translateY(0); }
.notification.hide { transform: translateY(-100%); }
}
`;
// 工具函数
const utils = {
// 等待DOM准备
waitForDOM: (callback) => {
if (document.body) {
callback();
} else {
document.addEventListener('DOMContentLoaded', callback);
}
},
// 防抖函数
debounce: (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
// 检查是否为PDF URL
isPdfUrl: (url) => {
return typeof url === 'string' &&
url.includes('cdn.pdfjm.cn') &&
url.includes('.pdf');
},
// 解码Base64数据
decodeBase64Data: (data) => {
try {
return atob(data);
} catch (error) {
console.error('Base64解码失败:', error);
return null;
}
}
};
// 通知系统
const notificationSystem = {
show: (message, type = 'info', duration = CONFIG.NOTIFICATION_DURATION.NORMAL) => {
if (!state.notificationContainer) {
notificationSystem.createContainer();
}
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
state.activeNotifications.push(notification);
state.notificationContainer.appendChild(notification);
// 显示动画
requestAnimationFrame(() => {
notification.classList.add('show');
});
// 自动隐藏
setTimeout(() => notificationSystem.hide(notification), duration);
},
hide: (notification) => {
if (!notification || !notification.parentNode) return;
notification.classList.remove('show');
notification.classList.add('hide');
const index = state.activeNotifications.indexOf(notification);
if (index > -1) {
state.activeNotifications.splice(index, 1);
}
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 400);
},
createContainer: () => {
if (state.notificationContainer) return;
state.notificationContainer = document.createElement('div');
state.notificationContainer.className = 'notification-container';
utils.waitForDOM(() => {
if (!document.body.contains(state.notificationContainer)) {
document.body.appendChild(state.notificationContainer);
}
});
}
};
// 按钮管理
const buttonManager = {
create: () => {
state.downloadButton = document.createElement('button');
state.downloadButton.className = 'pdf-download-btn';
buttonManager.update(CONFIG.BUTTON_TEXT.WAITING, true);
state.downloadButton.addEventListener('click', downloadManager.handle);
utils.waitForDOM(() => {
document.body.appendChild(state.downloadButton);
});
},
update: (text, disabled = false, loading = false) => {
if (!state.downloadButton) return;
state.downloadButton.disabled = disabled;
const icon = loading ?
'<div class="loading-spinner"></div>' :
'<svg class="icon" style="width:16px;height:16px" viewBox="0 0 24 24" fill="currentColor"><path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z" /></svg>';
state.downloadButton.innerHTML = `${icon} ${text}`;
}
};
// 下载管理
const downloadManager = {
handle: async () => {
if (!state.pdfBlob && !state.pdfUrl) {
notificationSystem.show('❌ 暂无可下载的PDF链接', 'error');
return;
}
buttonManager.update(CONFIG.BUTTON_TEXT.DOWNLOADING, true, true);
try {
let blob;
if (state.pdfBlob) {
blob = state.pdfBlob;
notificationSystem.show('⚡ 使用缓存数据,下载更快!', 'success', CONFIG.NOTIFICATION_DURATION.SHORT);
} else if (state.pdfUrl) {
notificationSystem.show('🔄 重新下载PDF文件...', 'warning');
blob = await downloadManager.fetchPdf(state.pdfUrl);
}
await downloadManager.triggerDownload(blob);
notificationSystem.show('🎉 PDF下载成功!', 'success');
buttonManager.update(CONFIG.BUTTON_TEXT.READY, false);
} catch (error) {
console.error('下载失败:', error);
notificationSystem.show(`❌ 下载失败: ${error.message}`, 'error', CONFIG.NOTIFICATION_DURATION.ERROR);
buttonManager.update(state.pdfBlob ? CONFIG.BUTTON_TEXT.READY : '下载PDF', false);
}
},
fetchPdf: async (url) => {
const response = await fetch(url, {
headers: {
"accept": "*/*",
"cache-control": "no-cache"
},
referrer: "https://pdfjm.cn/",
method: "GET",
mode: "cors",
credentials: "omit"
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.blob();
},
triggerDownload: async (blob) => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.download = "pdfjm下载报告.pdf";
a.href = url;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// 延迟清理URL以确保下载完成
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
};
// 网络拦截器
const interceptor = {
setupFetch: () => {
const originalFetch = window.fetch;
window.fetch = function(url, options = {}) {
if (utils.isPdfUrl(url)) {
console.log('检测到PDF文件请求:', url);
state.pdfUrl = url;
return originalFetch.apply(this, arguments).then(async response => {
const responseClone = response.clone();
try {
state.pdfBlob = await responseClone.blob();
console.log('成功缓存PDF blob,大小:', state.pdfBlob.size, 'bytes');
buttonManager.update(CONFIG.BUTTON_TEXT.READY, false);
notificationSystem.show('✅ PDF文件已缓存,可以下载!', 'success');
} catch (error) {
console.error('缓存PDF blob失败:', error);
}
return response;
});
}
return originalFetch.apply(this, arguments);
};
},
setupXHR: () => {
const originalXHR = window.XMLHttpRequest;
window.XMLHttpRequest = function() {
const xhr = new originalXHR();
const originalOpen = xhr.open;
xhr.open = function(method, url, ...args) {
this._url = url;
return originalOpen.apply(this, [method, url, ...args]);
};
const originalSend = xhr.send;
xhr.send = function(...args) {
if (this._url && this._url.includes('/api/pdf/uurl')) {
const originalOnReadyStateChange = this.onreadystatechange;
this.onreadystatechange = function() {
if (this.readyState === 4 && this.status === 200) {
try {
const response = JSON.parse(this.responseText);
if (response?.data) {
const decodedData = utils.decodeBase64Data(response.data);
if (decodedData && utils.isPdfUrl(decodedData)) {
state.pdfUrl = decodedData;
if (!state.pdfBlob) {
buttonManager.update(CONFIG.BUTTON_TEXT.LOADING, true);
notificationSystem.show('🔗 PDF链接已获取,等待文件加载...', 'info');
}
}
}
} catch (error) {
console.error('解析PDF响应失败:', error);
}
}
if (originalOnReadyStateChange) {
originalOnReadyStateChange.apply(this, arguments);
}
};
}
return originalSend.apply(this, args);
};
return xhr;
};
// 复制原型
Object.setPrototypeOf(window.XMLHttpRequest, originalXHR);
window.XMLHttpRequest.prototype = originalXHR.prototype;
}
};
// 初始化函数
function init() {
if (state.isInitialized) return;
console.log('PDF下载助手初始化开始');
// 注入样式
const style = document.createElement('style');
style.textContent = CSS_STYLES;
(document.head || document.documentElement).appendChild(style);
// 设置拦截器
interceptor.setupFetch();
interceptor.setupXHR();
// 创建UI
notificationSystem.createContainer();
buttonManager.create();
state.isInitialized = true;
console.log('PDF下载助手初始化完成');
notificationSystem.show('🚀 PDF下载助手已启动', 'success');
}
// 立即初始化
init();
})();