// ==UserScript== // @name 下载所有文件 // @namespace http://tampermonkey.net/ // @version 0.5 // @description try to take over the world! // @author You // @match http://*.zhbg.kjt.gx.gov/* // @grant GM_getValue // @run-at context-menu // ==/UserScript== (function() { 'use strict'; // 从本地存储获取数据(移除无用的s变量) const fileListStr = localStorage.getItem("FileList"); const data = JSON.parse(fileListStr); /** * 创建非模态进度对话框 * @param {number} total 总处理数量 * @returns {Object} 包含更新进度、绑定取消事件、关闭对话框的方法 */ function createProgressDialog(total) { // 创建对话框容器 const dialog = document.createElement('div'); dialog.id = 'download-progress-dialog'; dialog.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 15px rgba(0,0,0,0.2); z-index: 9999; min-width: 300px; font-family: Arial, sans-serif; box-sizing: border-box; `; // 标题 const title = document.createElement('h3'); title.textContent = '文件处理进度'; title.style.margin = '0 0 15px 0'; title.style.fontSize = '16px'; title.style.color = '#333'; dialog.appendChild(title); // 进度文本 const progressText = document.createElement('div'); progressText.id = 'progress-text'; progressText.style.margin = '0 0 10px 0'; progressText.style.color = '#666'; progressText.textContent = `正在处理 0/${total} 个元素`; dialog.appendChild(progressText); // 进度条容器 const progressBarContainer = document.createElement('div'); progressBarContainer.style.height = '8px'; progressBarContainer.style.background = '#eee'; progressBarContainer.style.borderRadius = '4px'; progressBarContainer.style.overflow = 'hidden'; dialog.appendChild(progressBarContainer); // 进度条 const progressBar = document.createElement('div'); progressBar.id = 'progress-bar'; progressBar.style.background = '#409eff'; progressBar.style.height = '100%'; progressBar.style.width = '0%'; progressBar.style.transition = 'width 0.3s ease'; progressBarContainer.appendChild(progressBar); // 取消按钮 const cancelBtn = document.createElement('button'); cancelBtn.textContent = '取消'; cancelBtn.style.marginTop = '15px'; cancelBtn.style.padding = '6px 12px'; cancelBtn.style.background = '#f56c6c'; cancelBtn.style.color = '#fff'; cancelBtn.style.border = 'none'; cancelBtn.style.borderRadius = '4px'; cancelBtn.style.cursor = 'pointer'; cancelBtn.style.fontSize = '14px'; cancelBtn.onmouseover = () => { cancelBtn.style.background = '#e64949'; }; cancelBtn.onmouseout = () => { cancelBtn.style.background = '#f56c6c'; }; dialog.appendChild(cancelBtn); // 添加到页面 document.body.appendChild(dialog); return { // 更新进度(核心修复:使用requestAnimationFrame强制重绘,异步更新) updateProgress: (current) => { // 使用requestAnimationFrame确保DOM更新在浏览器重绘周期执行 requestAnimationFrame(() => { const percent = (current / total) * 100; progressText.textContent = `正在处理 ${current}/${total} 个元素`; progressBar.style.width = `${percent}%`; // 强制触发重绘(兼容部分浏览器的性能优化) progressBar.offsetHeight; // 读取布局属性触发重绘 }); }, // 绑定取消事件 onCancel: (callback) => { cancelBtn.addEventListener('click', callback); }, // 关闭对话框 close: () => { if (document.body.contains(dialog)) { document.body.removeChild(dialog); } } }; } /** * 获取文件树数据 * @param {Object} sourceObj 源数据对象 * @returns {Promise} 文件树数据 */ async function getFileTreeData(sourceObj) { // 校验源对象是否有效(替换?.为&&) if (!sourceObj || typeof sourceObj !== 'object') { throw new Error('源数据对象不能为空且必须是对象类型'); } // 从源对象中提取需要的参数(替换?.为&&) const requestParams = { handleguid: (sourceObj && sourceObj.handleguid) || '', wdgid: (sourceObj && sourceObj.wdrowguid) || '', guid: (sourceObj && sourceObj.rowguid) || '', wdtype: (sourceObj && sourceObj.wdtype) || '', inboxguid: (sourceObj && sourceObj.inboxguid) || '', outboxguid: (sourceObj && sourceObj.outboxguid) || '' }; const url = 'http://dzgw.zhbg.kjt.gx.gov/bgtoa/rest/handleframeaction/getFileTree?isCommondto=true&action2rest=true'; try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'Accept': 'application/json, text/plain, */*' }, body: new URLSearchParams(requestParams) }); if (!response.ok) { throw new Error(`请求失败,状态码:${response.status} ${response.statusText}`); } return await response.json(); } catch (error) { console.error('获取文件树数据出错:', error); throw error; } } /** * 异步下载文件(使用XMLHttpRequest+Blob方式,静默下载不抢焦点) * @param {string} fullUrl 包含attachGuid的链接(用于提取fileId) * @param {string} fileName 自定义文件名 * @returns {Promise} */ async function downloadFileByFetch(fullUrl, fileName) { return new Promise((resolve, reject) => { try { // 1. 从fullUrl中提取attachGuid作为fileId(核心:和参考代码的guid对应) const reg = /attachGuid=([^&]+)/; const match = fullUrl.match(reg); const fileId = match ? match[1] : ''; if (!fileId) { throw new Error(`未从链接中提取到有效的fileId:${fullUrl}`); } // 2. 创建XMLHttpRequest请求(参考提供的代码) const httpRequest = new XMLHttpRequest(); httpRequest.open('POST', 'http://sxqp.zhbgzt.gx.gov/api-management/busi/fileService/downloadFile', true); // 3. 设置请求头(完全复用参考代码的头信息) httpRequest.setRequestHeader( "sxqp-header-user-info", "%7B%22operId%22%3A%22ui996q7o520ugvvtrgb48u%22%2C+%22operName%22%3A%22%E8%A9%B9%E5%BB%BA%22%2C+%22deptId%22%3A%225td76vu09xke4494x0rx82%22%2C+%22deptName%22%3A%22%E6%88%90%E6%9E%9C%E8%BD%AC%E5%8C%96%E4%B8%8E%E5%8C%BA%E5%9F%9F%E5%88%9B%E6%96%B0%E5%A4%84%22%2C+%22tenantId%22%3A%22%22%7D" ); httpRequest.setRequestHeader("Content-type", "application/json;charset=utf-8"); // 4. 设置Cookie(复用参考代码的Cookie) document.cookie = "BGZHTBGZUULSESSIONZHONGTAI=feb34a41-3d8a-4afa-8a16-0a5ef8e6e300"; // 5. 设置响应类型为Blob(二进制文件) httpRequest.responseType = "blob"; // 6. 处理请求状态变化 httpRequest.onreadystatechange = function () { if (httpRequest.readyState === 4) { // 请求完成 if (httpRequest.status === 200) { // 响应成功 // 从响应头提取文件名(兼容不同格式的content-disposition) let respFileName = ''; const disposition = httpRequest.getResponseHeader("content-disposition"); if (disposition) { respFileName = decodeURIComponent(disposition); // 兼容 filename=xxx 或 filename*=UTF-8''xxx 格式 const fileNameMatch = respFileName.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/); if (fileNameMatch && fileNameMatch[1]) { respFileName = fileNameMatch[1].replace(/['"]/g, ''); } else { respFileName = respFileName.substring(respFileName.indexOf('=') + 1) || ''; } } // 优先使用自定义文件名,无则用响应头的,最后兜底 const finalFileName = fileName || respFileName || `文件_${fileId}`; // 7. 创建a标签下载Blob(静默无焦点) const a = document.createElement('a'); a.download = finalFileName; a.href = URL.createObjectURL(httpRequest.response); // 创建Blob URL a.style.display = 'none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); // 8. 释放Blob URL,避免内存泄漏 URL.revokeObjectURL(a.href); console.log(`✅ 触发下载:${finalFileName},fileId:${fileId}`); resolve(); } else { // 响应失败处理 const errorMsg = `下载失败,状态码:${httpRequest.status}(${httpRequest.statusText})`; console.error(`❌ 下载${fileName}出错:`, errorMsg); alert(`文件${fileName}下载失败:${errorMsg}`); reject(new Error(errorMsg)); } } }; // 9. 处理网络错误(如断网、跨域等) httpRequest.onerror = function () { const errorMsg = '网络请求错误,无法连接下载服务器'; console.error(`❌ 下载${fileName}出错:`, errorMsg); alert(`文件${fileName}下载失败:${errorMsg}`); reject(new Error(errorMsg)); }; // 10. 发送请求体(JSON格式传fileId) httpRequest.send(JSON.stringify({ fileId: fileId })); } catch (error) { console.error(`❌ 下载${fileName}出错:`, error); alert(`文件${fileName}下载失败,请稍后重试`); reject(error); } }); } /** * 处理下载队列(逐个执行,间隔5秒) * @param {Array<{url: string, name: string}>} tasks 下载任务列表 */ async function processDownloadQueue(tasks) { for (const task of tasks) { try { await downloadFileByFetch(task.url, task.name); // 每个文件下载后间隔5秒(改为异步延迟,不阻塞主线程) await new Promise(resolve => setTimeout(resolve, 5000)); } catch (error) { // 单个文件失败不中断队列,仍等待5秒继续下一个 await new Promise(resolve => setTimeout(resolve, 5000)); } } console.log('🎉 所有下载任务执行完成'); } function keepOnlyNumbers(str) { // 先判断输入是否为字符串,非字符串则转为空字符串 const inputStr = typeof str === 'string' ? str : ''; // \D 匹配所有非数字字符,g 全局替换,替换为空 return inputStr.replace(/\D/g, ''); } /** * 获取已下载的办文编号列表 * @returns {Array} 已下载的办文编号数组 */ function getDownloadedBanwenNumbers() { const storedData = localStorage.getItem('downloaded_banwennumbers'); // 容错处理:确保返回的是数组 return storedData ? (Array.isArray(JSON.parse(storedData)) ? JSON.parse(storedData) : []) : []; } /** * 保存办文编号到已下载列表 * @param {string} banwennumber 办文编号(仅数字) */ function saveDownloadedBanwenNumber(banwennumber) { if (!banwennumber || typeof banwennumber !== 'string') return; const downloadedList = getDownloadedBanwenNumbers(); // 避免重复添加 if (!downloadedList.includes(banwennumber)) { downloadedList.push(banwennumber); localStorage.setItem('downloaded_banwennumbers', JSON.stringify(downloadedList)); } } /** * 提取所有下载任务(不再直接下载,而是返回任务列表) * @param {Object} sourceData 源数据 * @returns {Array<{url: string, name: string}>} 下载任务列表 */ function extractAllUrls(sourceData) { const downloadTasks = []; // 移除可选链?.,替换为ES5兼容的&&判断 if (!sourceData || !sourceData.fileList || !Array.isArray(sourceData.fileList)) { console.warn('文件列表为空或格式错误'); return downloadTasks; } sourceData.fileList.forEach(item => { if (Array.isArray(item.files)) { item.files.forEach(file => { // 校验文件url有效性(移除?.) if (!file || typeof file.url !== 'string' || file.url.trim() === '') return; // 构建下载链接(保留原有逻辑,仅用于提取attachGuid)和文件名 const fullUrl = file.url; const fileName = `${keepOnlyNumbers(sourceData.banwennumber)}_${file.title}`; // 添加到下载任务队列 downloadTasks.push({ url: fullUrl, name: fileName }); }); } }); return downloadTasks; } /** * 主处理流程:遍历data所有元素,逐个处理 */ async function mainProcess() { // 校验data是否为数组 if (!Array.isArray(data)) { console.error('FileList数据格式错误,不是数组'); return; } const total = data.length; if (total === 0) { console.warn('data数组为空'); return; } // 创建进度对话框 const progressDialog = createProgressDialog(total); let isCancelled = false; // 绑定取消事件:点击后标记取消状态并关闭对话框 progressDialog.onCancel(() => { isCancelled = true; progressDialog.close(); console.log('用户取消了处理流程'); }); // 遍历data中的每个元素(改为索引遍历,方便更新进度) for (let i = 0; i < data.length; i++) { // 先更新进度(核心修复:提前更新进度,避免下载阻塞导致进度延迟) progressDialog.updateProgress(i + 1); const item = data[i]; // 检查是否取消,若取消则终止遍历 if (isCancelled) { break; } // ===== 新增:检查办文编号是否已下载 ===== // 提取并格式化办文编号(和原有文件名处理逻辑一致) const banwennumber = item && item.banwennumber ? keepOnlyNumbers(item.banwennumber) : ''; const downloadedList = getDownloadedBanwenNumbers(); // 如果办文编号有效且已下载,跳过处理该元素 if (banwennumber && downloadedList.includes(banwennumber)) { console.log(`📌 办文编号 ${banwennumber} 已下载,跳过处理该元素`); continue; } // ====================================== try { console.log(`📄 开始处理data元素 ${i+1}/${total}:`, item); // 获取文件树数据 const treeData = await getFileTreeData(item); // 提取下载任务(补充treeData.custom的&&判断) const downloadTasks = extractAllUrls(treeData && treeData.custom ? treeData.custom : {}); // 执行下载队列(间隔5秒) if (downloadTasks.length > 0) { await processDownloadQueue(downloadTasks); } else { console.log('⚠️ 该元素无下载任务'); } // ===== 新增:处理完成后标记办文编号为已下载 ===== if (banwennumber) { saveDownloadedBanwenNumber(banwennumber); console.log(`✅ 办文编号 ${banwennumber} 已标记为已下载`); } // ============================================== } catch (err) { console.error(`❌ 处理data元素 ${i+1}/${total} 失败:`, err); // 单个元素处理失败,继续处理下一个 continue; } } // 处理完成后关闭对话框(未被取消的情况) if (!isCancelled) { progressDialog.close(); console.log('🎉 所有data元素处理完成'); } } // 启动主流程(包裹在异步函数中,避免阻塞) (async () => { await mainProcess(); })().catch(err => { console.error('🚨 主流程执行失败:', err); // 若出错,关闭可能残留的对话框 const dialog = document.getElementById('download-progress-dialog'); if (dialog) { document.body.removeChild(dialog); } }); })();