You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中如何实现Power BI报表的PDF下载?

解决方案:Power BI报表PDF导出下载问题修复

问题核心分析

你的代码通过拦截.pdf结尾的导航请求触发下载,但Power BI导出PDF的常规流程并非直接跳转至PDF URL,而是通过JavaScript生成Blob对象后触发前端下载,这种场景下onNavigationRequest无法捕获请求。另外,直接用dio下载时,会缺失WebView中的Power BI认证Cookie,导致下载内容异常或权限验证失败。


步骤1:替换导航拦截为WebView下载处理器

使用WebView原生的setDownloadHandlerAPI替代导航拦截,它能直接捕获WebView内所有下载请求(包括Blob生成的下载):

修改_initializeWebView方法:

void _initializeWebView() {
  controller = WebViewController()
    ..setJavaScriptMode(JavaScriptMode.unrestricted)
    ..setBackgroundColor(const Color(0x00000000))
    ..setDownloadHandler((DownloadRequest request) async {
      // 捕获所有下载请求并触发下载逻辑
      await _downloadPdf(request.url);
      return true; // 返回true表示已接管下载处理
    })
    ..setNavigationDelegate(
      NavigationDelegate(
        onProgress: (int progress) {},
        onPageStarted: (String url) {},
        onPageFinished: (String url) {},
        onWebResourceError: (WebResourceError error) {
          print('Error loading page: ${error.description}');
        },
        // 移除原有的.pdf后缀拦截逻辑
      ),
    )
    ..loadRequest(Uri.parse(reportUrl));
}

步骤2:携带Power BI认证Cookie下载

直接用dio下载时,WebView的会话Cookie不会自动携带,导致Power BI返回权限错误。需先从WebView中获取Cookie,再添加到dio请求头:

修改_downloadPdf方法:

Future<void> _downloadPdf(String url) async {
  // 适配Android 13+存储权限
  Permission storagePermission;
  if (Platform.isAndroid && await AndroidInfo.version >= 13) {
    storagePermission = Permission.manageExternalStorage;
  } else {
    storagePermission = Permission.storage;
  }

  if (await storagePermission.request().isGranted) {
    var dio = Dio();
    try {
      // 从WebView获取当前会话Cookie
      String? cookie = await controller.runJavaScriptReturningResult(
        "document.cookie"
      ) as String?;

      // 添加Cookie到请求头
      if (cookie != null && cookie.isNotEmpty) {
        dio.options.headers["Cookie"] = cookie;
      }

      // 获取公共下载目录,方便用户查找文件
      Directory? downloadDir;
      if (Platform.isAndroid) {
        downloadDir = await getExternalStorageDirectory();
        // Android 13+ 可引入path_provider_extras包使用getDownloadsDirectory()
      } else if (Platform.isIOS) {
        downloadDir = await getApplicationDocumentsDirectory();
      }
      
      if (downloadDir == null) {
        showToastMessage('无法获取存储目录');
        return;
      }

      // 生成唯一文件名,避免重复
      String fileName = url.split('/').last;
      if (!fileName.endsWith('.pdf')) {
        fileName = 'Sales_Report_${DateTime.now().millisecondsSinceEpoch}.pdf';
      }
      String filePath = '${downloadDir.path}/$fileName';

      await dio.download(url, filePath);
      showToastMessage('PDF已保存至:$filePath');
    } catch (e) {
      print('下载错误:$e');
      showToastMessage('下载失败,请重试');
    }
  } else {
    showToastMessage('存储权限被拒绝');
  }
}

步骤3:适配Android 13+存储权限配置

Android 13及以上版本废弃了WRITE_EXTERNAL_STORAGE权限,如需保存到公共目录,需在AndroidManifest.xml中添加对应权限:

<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />

步骤4:处理Power BI Blob导出场景

如果Power BI通过Blob生成PDF(无直接PDF URL),需注入JavaScript拦截Blob下载事件,将Blob转为Base64后传递给Flutter原生处理:

在onPageFinished中注入JS并注册回调:

onPageFinished: (String url) {
  // 注入JS拦截Blob类型的PDF下载
  controller.runJavaScript('''
    // 重写XMLHttpRequest监听Blob响应
    const originalXHRSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = function() {
      this.addEventListener('load', function() {
        if (this.responseType === 'blob' && this.getResponseHeader('Content-Type') === 'application/pdf') {
          const reader = new FileReader();
          reader.onload = function() {
            window.flutterPdfHandler.postMessage(reader.result);
          };
          reader.readAsDataURL(this.response);
        }
      });
      originalXHRSend.apply(this, arguments);
    };

    // 重写fetch监听Blob响应
    const originalFetch = fetch;
    window.fetch = async function() {
      const response = await originalFetch.apply(this, arguments);
      if (response.headers.get('Content-Type') === 'application/pdf') {
        const blob = await response.blob();
        const reader = new FileReader();
        reader.onload = function() {
          window.flutterPdfHandler.postMessage(reader.result);
        };
        reader.readAsDataURL(blob);
      }
      return response;
    };
  ''');

  // 注册Flutter回调接收Base64格式PDF
  controller.addJavaScriptChannel(
    name: 'flutterPdfHandler',
    onMessageReceived: (JavaScriptMessage message) async {
      if (message.message.startsWith('data:application/pdf;base64,')) {
        String base64Pdf = message.message.split(',')[1];
        await _saveBase64Pdf(base64Pdf);
      }
    },
  );
},

添加Base64 PDF保存方法:

Future<void> _saveBase64Pdf(String base64Pdf) async {
  Permission storagePermission;
  if (Platform.isAndroid && await AndroidInfo.version >= 13) {
    storagePermission = Permission.manageExternalStorage;
  } else {
    storagePermission = Permission.storage;
  }

  if (await storagePermission.request().isGranted) {
    try {
      Directory? downloadDir;
      if (Platform.isAndroid) {
        downloadDir = await getExternalStorageDirectory();
      } else if (Platform.isIOS) {
        downloadDir = await getApplicationDocumentsDirectory();
      }
      
      if (downloadDir == null) {
        showToastMessage('无法获取存储目录');
        return;
      }

      String fileName = 'Sales_Report_${DateTime.now().millisecondsSinceEpoch}.pdf';
      String filePath = '${downloadDir.path}/$fileName';

      // 将Base64解码为文件
      Uint8List bytes = base64Decode(base64Pdf);
      await File(filePath).writeAsBytes(bytes);

      showToastMessage('PDF已保存至:$filePath');
    } catch (e) {
      print('保存错误:$e');
      showToastMessage('保存失败,请重试');
    }
  } else {
    showToastMessage('存储权限被拒绝');
  }
}

验证顺序

  1. 优先测试setDownloadHandler逻辑,这是覆盖大多数场景的解决方案。
  2. 若下载失败,检查控制台打印的Cookie是否有效,确认权限验证是否通过。
  3. 若Power BI为Blob导出方式,启用JS拦截逻辑处理Base64内容。

内容的提问来源于stack exchange,提问作者Jay Patel

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 21:33:12