MENU

[泛微 E10] 批量下载当前表单内所有附件

• 2025 年 07 月 07 日 • 阅读: 7531 • OA

功能背景

泛微 E10 支持单附件字段的批量下载能力。但默认情况下,用户只能逐个字段进行批量下载附件,当表单包含大量附件字段时,操作效率较低。通过开发批量下载功能,提升用户体验。

技术实现方案

下面是实现批量下载功能的核心代码,主要分为三个步骤:收集附件 ID、创建异步下载任务、轮询检查任务状态并下载:

// 初始化 SDK 实例
const weFormSdk = window.WeFormSDK.getWeFormInstance(); 

// 定义需要下载附件的字段名称,请修改为对应的主表字段名称
const fieldNames = [
    "sigvendatf",
    "poa",
    "regcer",
    "taxcer",
    "pscq",
    "pscend",
    "bancon",
    "conpo",
    "emaappandc"
];

// 收集所有附件 ID
const values = [];
fieldNames.forEach(fieldName => {
    try {
        const fieldId = weFormSdk.convertFieldNameToId(fieldName);
        const fieldValue = weFormSdk.getFieldValue(fieldId);
        if (fieldValue) { 
            values.push(fieldValue);
        }
    } catch (error) {
        console.error(` 获取字段 ${fieldName} 失败:`, error);
    }
});

// 创建异步下载任务
async function createDownloadTask() {
    if (values.length === 0) {
        window.WeFormSDK.showMessage("没有可下载的文件", 1, 3);
        return;
    }
    
    const today = new Date().toISOString().split('T')[0]; // 获取当前日期
    
    try {
        const response = await fetch('/api/file/batch/asynchronous/downloadByFileIds', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                "module": "ebuildercard",
                "fileIds": values.join(','),
                "authModule": "ebuildercard",
                "source": "form",
                "zipName": `附件名称在这里-请修改或赋值变量-${today}`
            })
        });
        
        const data = await response.json();
        if (!data.data.taskId) {
            throw new Error('获取任务 ID 失败');
        }
        
        window.WeFormSDK.showMessage("开始创建下载任务", 3, 2);
        await checkDownloadStatus(data.data.taskId);
    } catch (error) {
        console.error('创建下载任务失败:', error);
        window.WeFormSDK.showMessage("创建下载任务失败", 2, 3);
    }
}

// 轮询检查下载任务状态
async function checkDownloadStatus(taskId) {
    let attempt = 0;
    const maxAttempts = 20; // 最多尝试 20 次
    
    while (attempt < maxAttempts) {
        try {
            const response = await fetch(`/api/file/batchDownloadPercent?uuid=${taskId}`);
            const data = await response.json();
            
            if (data.data.fileId) {
                window.WeFormSDK.showMessage("下载任务已完成", 3, 2);
                downloadFile(data.data.fileId);
                return;
            }
            
            attempt++;
            if (attempt < maxAttempts) {
                await new Promise(resolve => setTimeout(resolve, 500)); // 等待 500ms
            }
        } catch (error) {
            console.error('查询下载状态失败:', error);
            throw error;
        }
    }
    
    throw new Error('下载任务超时');
}

// 执行文件下载
function downloadFile(fileId) {
    const link = document.createElement('a');
    link.href = `/api/file/remotedownload/${fileId}/upload/true`;
    link.setAttribute('download', '');
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
}

// 启动下载流程
createDownloadTask().catch(error => {
    console.error('下载流程出错:', error);
    window.WeFormSDK.showMessage("下载过程中出现错误", 2, 3);
});

代码功能解析

  1. 附件收集模块

    • 通过 weFormSdk.convertFieldNameToId 方法将字段名称转换为系统 ID
    • 使用 weFormSdk.getFieldValue 获取附件 ID 值
    • 支持从多个字段收集附件,适应复杂表单结构
  2. 异步下载任务创建

    • 调用平台提供的 /api/file/batch/asynchronous/downloadByFileIds 接口
    • 将所有附件 ID 合并后作为参数传递
    • 支持自定义 ZIP 文件名,自动添加日期戳
  3. 任务状态监控机制

    • 通过轮询 /api/file/batchDownloadPercent 接口检查进度
    • 设置最大尝试次数和间隔时间,防止无限循环
    • 任务完成后自动触发下载
  4. 用户体验优化

    • 使用平台 SDK 提供的消息提示功能
    • 完整的错误处理机制
    • 异步操作避免界面卡顿
添加新评论

已有 2 条评论
  1. titi titi

    请问E9要怎么实现这个功能呢

    1. 路人甲 路人甲

      @titiconst { Button, message } = antd;
      const { WeaDialog, WeaTable, } = ecCom;
      class NewReqTopButtonCom extends React.Component {
      constructor(props) {

      super(props); this.state = {};

      }

      componentDidMount() {

      ecodeSDK.setCom('${appId}', 'NewReqTopButtonComInstance', this);

      }

      componentWillUnmount() {

      ecodeSDK.setCom('${appId}', 'NewReqTopButtonComInstance', null);

      }

      // 提供给父组件调用的方法
      triggerModal(propVisible, propRequestId) {

      // 直接调用批量下载方法 this.handleBatchDownload();

      }

      // 检查是否为纯数字ID
      isPureNumericId = (id) => {

      return id && /^\d+$/.test(id);

      }

      // 批量下载方法
      handleBatchDownload = async () => {

      try { message.info('正在解析附件...'); const attachments = this.extractAttachments(); if (attachments.length === 0) { message.warning('未找到可下载的附件!'); return; } // 显示找到的附件信息 const confirmMsg = `已找到 ${attachments.length} 个附件:\n${attachments.map((att, idx) => ` ${idx + 1}. ${att.filename}`).join('\n')}\n\n是否开始批量下载?`; if (!confirm(confirmMsg)) { return; } message.info(`准备下载,共 ${attachments.length} 个文件`); await this.downloadAttachments(attachments); message.success(`批量下载任务已启动,共 ${attachments.length} 个文件。\n请注意检查浏览器下载列表。`); } catch (error) { console.error('批量下载出错:', error); message.error('下载过程中出现错误: ' + error.message); }

      }

      // 从URL中提取fileid - 修改为只返回纯数字ID
      extractFileId = (url) => {

      try { // 尝试解析URL const urlObj = new URL(url); // 优先获取fileid参数 let fileId = urlObj.searchParams.get('fileid'); // 检查是否为纯数字 if (fileId && this.isPureNumericId(fileId)) { return fileId; } // 如果没有有效的fileid,尝试获取imagefileId fileId = urlObj.searchParams.get('imagefileId'); // 检查是否为纯数字 if (fileId && this.isPureNumericId(fileId)) { return fileId; } return null; } catch (e) { // 如果URL解析失败,尝试使用正则表达式 const fileIdRegex = /(?:fileid|imagefileId)=([^&]+)/; const match = url.match(fileIdRegex); const extractedId = match ? match[1] : null; // 只返回纯数字ID return extractedId && this.isPureNumericId(extractedId) ? extractedId : null; }

      }

      // 附件提取方法 - 只获取纯数字ID的链接
      extractAttachments = () => {

      const attachments = []; const seenFileIds = new Set(); // 用于根据fileid去重 // 选择所有可能的附件链接元素 const selectors = [ 'a[href*="fileid="]', 'a[href*="imagefileId="]', 'img[src*="fileid="]', 'img[src*="imagefileId="]', 'input[value*="fileid="]', 'input[value*="imagefileId="]', 'a[href*="/weaver/weaver.file.FileDownload"]', 'a[href*="/spa/document/"]' ]; selectors.forEach(selector => { const elements = document.querySelectorAll(selector); elements.forEach(element => { let url = ''; // 获取URL if (element.href) { url = element.href; } else if (element.src) { url = element.src; } else if (element.value) { url = element.value; } // 提取fileid - 现在只返回纯数字ID const fileId = this.extractFileId(url); // 只处理包含有效纯数字fileid的链接 if (url && fileId) { // 根据fileid去重 if (seenFileIds.has(fileId)) { console.log(`跳过重复文件: ${fileId}`); return; } seenFileIds.add(fileId); // 构建完整的下载URL - 保留原始URL中的所有参数 const downloadUrl = this.buildDownloadUrl(url, fileId); // 提取文件名 const filename = this.extractFilename(element, fileId); attachments.push({ url: downloadUrl, filename: this.sanitizeFilename(filename), fileId: fileId, element: element }); } }); }); console.log(`去重后找到 ${attachments.length} 个唯一附件`); console.log('附件列表:', attachments.map(att => ({ fileId: att.fileId, filename: att.filename }))); return attachments;

      }

      // 构建下载URL - 保留所有必要的认证参数
      buildDownloadUrl = (originalUrl, fileId) => {

      try { // 如果已经是完整的下载链接,直接使用 if (originalUrl.includes('/weaver/weaver.file.FileDownload') && originalUrl.includes('download=1')) { return originalUrl; } // 否则,构建新的下载URL,但保留原始URL中的认证参数 const urlObj = new URL(originalUrl); const authParams = {}; // 提取可能的认证参数 const authParamNames = [ 'authStr', 'authSignatureStr', 'requestid', 'desrequestid', 'f_weaver_belongto_userid', 'f_weaver_belongto_usertype', 'fromrequest' ]; authParamNames.forEach(param => { const value = urlObj.searchParams.get(param); if (value) { authParams[param] = value; } }); // 构建基础下载URL let downloadUrl = `http://oa.fshbslaser.com:88/weaver/weaver.file.FileDownload?download=1&fileid=${fileId}`; // 添加认证参数 Object.keys(authParams).forEach(key => { downloadUrl += `&${key}=${encodeURIComponent(authParams[key])}`; }); return downloadUrl; } catch (e) { // 如果URL构建失败,使用简单的下载URL return `http://oa.fshbslaser.com:88/weaver/weaver.file.FileDownload?download=1&fileid=${fileId}`; }

      }

      // 改进文件名提取方法
      extractFilename = (element, fileId) => {

      // 首先尝试从元素的直接文本获取 let filename = element.textContent ? element.textContent.trim() : ''; // 检查文件名是否有效 if (this.isValidFilename(filename)) { return filename; } // 尝试从title属性获取 if (element.getAttribute('title')) { const title = element.getAttribute('title').trim(); if (this.isValidFilename(title)) { return title; } } // 尝试从alt属性获取(对于img元素) if (element.getAttribute('alt')) { const alt = element.getAttribute('alt').trim(); if (this.isValidFilename(alt)) { return alt; } } // 尝试从data属性获取 if (element.dataset) { const dataKeys = Object.keys(element.dataset); for (const key of dataKeys) { if (key.includes('name') || key.includes('filename')) { const dataValue = element.dataset[key]; if (this.isValidFilename(dataValue)) { return dataValue; } } } } // 尝试从父元素中查找文件名 const parent = element.closest('.attachment-item, .file-item, .wea-field-link, .reqresnameclass'); if (parent) { // 查找常见的文件名元素 const nameSelectors = ['.file-name', '.filename', '.attachment-name', '.wea-field-text', '.reqresnameclass']; for (const selector of nameSelectors) { const nameEl = parent.querySelector(selector); if (nameEl) { const text = nameEl.textContent ? nameEl.textContent.trim() : ''; if (this.isValidFilename(text)) { return text; } } } } // 如果所有方法都失败,使用fileId作为文件名 return `附件_${fileId}`;

      }

      // 检查文件名是否有效
      isValidFilename = (filename) => {

      return filename && filename.length > 2 && filename.length < 100 && !filename.includes('fileid=') && !filename.includes('imagefileId=') && !filename.startsWith('_') && !filename.includes('weaver.file.FileDownload');

      }

      sanitizeFilename = (filename) => {

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

      }

      // 下载方法
      downloadAttachments = async (attachments) => {

      const BATCH_SIZE = 2; // 每批下载2个 const DELAY_BETWEEN_BATCHES = 2000; // 批次间隔2秒 const DELAY_BETWEEN_DOWNLOADS = 500; // 单个下载间隔0.5秒 for (let i = 0; i < attachments.length; i += BATCH_SIZE) { const batch = attachments.slice(i, i + BATCH_SIZE); for (let j = 0; j < batch.length; j++) { await this.downloadSingleFile(batch[j], i + j + 1, attachments.length); // 同一批次内稍作延迟 if (j < batch.length - 1) { await this.delay(DELAY_BETWEEN_DOWNLOADS); } } // 如果不是最后一批,等待一段时间再处理下一批 if (i + BATCH_SIZE < attachments.length) { await this.delay(DELAY_BETWEEN_BATCHES); } }

      }

      downloadSingleFile = (attachment, currentIndex, totalCount) => {

      return new Promise((resolve) => { try { console.log(`下载: ${attachment.filename} (ID: ${attachment.fileId})`); console.log(`下载URL: ${attachment.url}`); // 使用 iframe 方式下载 this.downloadViaIframe(attachment); resolve(); } catch (error) { console.error(`下载失败 ${attachment.filename}:`, error); resolve(); } });

      }

      downloadViaIframe = (attachment) => {

      try { // 创建隐藏的iframe进行下载 const iframe = document.createElement('iframe'); iframe.style.display = 'none'; iframe.src = attachment.url; document.body.appendChild(iframe); // 清理iframe setTimeout(() => { if (document.body.contains(iframe)) { document.body.removeChild(iframe); } }, 30000); // 30秒后清理 } catch (error) { console.error(`iframe下载失败: ${error}`); }

      }

      delay = (ms) => {

      return new Promise(resolve => setTimeout(resolve, ms));

      }

      render() {

      return null;

      }
      }

      //发布模块
      ecodeSDK.setCom('${appId}', 'NewReqTopButtonCom', NewReqTopButtonCom);,触发按钮,用了官方应用里的《流程表单新增按钮并点击触发列表弹框》