功能背景
泛微 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);
});代码功能解析
附件收集模块
- 通过
weFormSdk.convertFieldNameToId方法将字段名称转换为系统 ID - 使用
weFormSdk.getFieldValue获取附件 ID 值 - 支持从多个字段收集附件,适应复杂表单结构
- 通过
异步下载任务创建
- 调用平台提供的
/api/file/batch/asynchronous/downloadByFileIds接口 - 将所有附件 ID 合并后作为参数传递
- 支持自定义 ZIP 文件名,自动添加日期戳
- 调用平台提供的
任务状态监控机制
- 通过轮询
/api/file/batchDownloadPercent接口检查进度 - 设置最大尝试次数和间隔时间,防止无限循环
- 任务完成后自动触发下载
- 通过轮询
用户体验优化
- 使用平台 SDK 提供的消息提示功能
- 完整的错误处理机制
- 异步操作避免界面卡顿
[泛微 E10] 批量下载当前表单内所有附件 by https://oneszhang.com/archives/164.html
请问E9要怎么实现这个功能呢
const { Button, message } = antd;
super(props); this.state = {};const { WeaDialog, WeaTable, } = ecCom;
class NewReqTopButtonCom extends React.Component {
constructor(props) {
}
componentDidMount() {
ecodeSDK.setCom('${appId}', 'NewReqTopButtonComInstance', this);}
componentWillUnmount() {
ecodeSDK.setCom('${appId}', 'NewReqTopButtonComInstance', null);}
// 提供给父组件调用的方法
// 直接调用批量下载方法 this.handleBatchDownload();triggerModal(propVisible, propRequestId) {
}
// 检查是否为纯数字ID
return id && /^\d+$/.test(id);isPureNumericId = (id) => {
}
// 批量下载方法
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); }handleBatchDownload = async () => {
}
// 从URL中提取fileid - 修改为只返回纯数字ID
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; }extractFileId = (url) => {
}
// 附件提取方法 - 只获取纯数字ID的链接
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;extractAttachments = () => {
}
// 构建下载URL - 保留所有必要的认证参数
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}`; }buildDownloadUrl = (originalUrl, 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}`;extractFilename = (element, fileId) => {
}
// 检查文件名是否有效
return filename && filename.length > 2 && filename.length < 100 && !filename.includes('fileid=') && !filename.includes('imagefileId=') && !filename.startsWith('_') && !filename.includes('weaver.file.FileDownload');isValidFilename = (filename) => {
}
sanitizeFilename = (filename) => {
// 移除文件名中的非法字符 return filename.replace(/[/\\?%*:|"<>]/g, '_');}
// 下载方法
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); } }downloadAttachments = async (attachments) => {
}
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);,触发按钮,用了官方应用里的《流程表单新增按钮并点击触发列表弹框》