Skip to content
javascript
/**
 * 通过 URL 下载文件(使用 a 标签)
 * @param {string} url - 文件地址
 * @param {string} [fileName] - 文件名
 */
export function downloadUrl(url, fileName) {
  const a = document.createElement('a');
  a.href = url;

  if (fileName) {
    a.download = fileName;
  }

  a.style.display = 'none';
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
}

/**
 * 文件下载
 * @param {object} axiosInstance - axios实例
 * @param {string} url
 * @param {string} method - 请求方法,默认 'post'
 * @param {object} data - 请求参数
 * @param {string} [fileName] - 文件名(可选)
 * @param {function} [onProgress] - 下载进度回调函数 (percent, loaded, total) => {}
 * @returns {Promise<string>} 返回下载的文件名
 */
export function downloadFile(axiosInstance, url, method = 'post', data = {}, fileName, onProgress) {
  return axiosInstance({
    url,
    method,
    data,
    responseType: 'blob',
    onDownloadProgress: progressEvent => {
      if (progressEvent.lengthComputable) {
        const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100);
        onProgress?.(percent, progressEvent.loaded, progressEvent.total);
      }
    }
  }).then(response => {
    // 拦截器处理后,blob 响应返回 { data, headers } 对象

    let responseData = null;
    let responseHeaders = {};

    if (response && typeof response === 'object') {
      if (response.data instanceof Blob) {
        responseData = response.data;
        responseHeaders = response.headers || {};
      } else if (response instanceof Blob) {
        // 如果直接返回 blob
        responseData = response;
      } else {
        responseData = response.data;
        responseHeaders = response.headers || {};
      }
    } else {
      responseData = response;
    }

    // 获取文件名
    let finalFileName = fileName;
    const disposition = responseHeaders['content-disposition'];

    if (!finalFileName && disposition) {
      // 尝试从 content-disposition 获取文件名
      const utf8Filename = disposition.match(/filename\*=UTF-8''(.+)/i);
      if (utf8Filename?.[1]) {
        finalFileName = decodeURIComponent(utf8Filename[1]);
      } else {
        const asciiFilename = disposition.match(/filename=["']?([^"']*)["']?/i);
        if (asciiFilename?.[1]) {
          try {
            // 解码(可能是 UTF-8 URL 编码)
            finalFileName = decodeURIComponent(asciiFilename[1]);
          } catch {
            // 解码失败, 使用原始值
            finalFileName = asciiFilename[1];
          }
        }
      }
    }

    // 兜底:文件名使用时间戳
    if (!finalFileName) {
      const now = new Date();
      const year = now.getFullYear();
      const month = String(now.getMonth() + 1).padStart(2, '0');
      const day = String(now.getDate()).padStart(2, '0');
      const hours = String(now.getHours()).padStart(2, '0');
      const minutes = String(now.getMinutes()).padStart(2, '0');
      const seconds = String(now.getSeconds()).padStart(2, '0');
      finalFileName = `${year}${month}${day}${hours}${minutes}${seconds}`;
    }

    // 获取文件类型
    const contentType = responseHeaders['content-type'] || '';
    const isCSV = contentType.includes('csv');
    const fileExtension = isCSV ? 'csv' : contentType;

    // 如果文件名没有扩展名,添加扩展名
    if (!finalFileName.includes('.')) {
      finalFileName = `${finalFileName}.${fileExtension}`;
    }

    // 下载
    const blob = responseData instanceof Blob ? responseData : new Blob([responseData], { type: contentType });
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = finalFileName;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(a.href);

    return finalFileName;
  });
}

Released under the MIT License.