Ant Design Pro数据导出功能:Excel/CSV格式实现方案

Ant Design Pro数据导出功能:Excel/CSV格式实现方案

【免费下载链接】ant-design-pro 👨🏻‍💻👩🏻‍💻 Use Ant Design like a Pro! 【免费下载链接】ant-design-pro 项目地址: https://gitcode.com/gh_mirrors/an/ant-design-pro

痛点与解决方案概述

你是否还在为Ant Design Pro项目中数据导出功能的实现而烦恼?是否需要同时支持Excel和CSV两种格式,却不知从何下手?本文将提供一套完整的解决方案,帮助你在Ant Design Pro项目中快速集成专业级的数据导出功能。读完本文,你将能够:

  • 掌握在Ant Design Pro中实现Excel/CSV导出的核心技术
  • 理解前后端数据处理的完整流程
  • 学会自定义导出格式和样式
  • 解决大文件导出时的性能问题
  • 实现国际化和权限控制的导出功能

技术选型与架构设计

导出功能技术栈对比

技术方案优点缺点适用场景
纯前端导出无需后端参与,响应速度快处理大量数据时可能卡顿数据量较小(<1万条)的场景
纯后端导出可处理大量数据,不占用前端资源增加服务器负担,需要等待生成文件数据量较大(>1万条)的场景
前后端结合兼顾性能与用户体验实现复杂度较高对用户体验要求高的中大型应用

架构设计流程图

mermaid

实现步骤

1. 安装必要依赖

首先,我们需要安装处理Excel和文件下载的相关依赖:

npm install xlsx file-saver --save

2. 创建导出工具类

src/utils目录下创建exportUtils.ts文件,封装导出相关的工具函数:

import { saveAs } from 'file-saver';
import XLSX from 'xlsx';
import { message } from 'antd';
import { useIntl } from '@umijs/max';

// 定义导出选项接口
export interface ExportOptions {
  /** 导出数据 */
  data: Record<string, any>[];
  /** 导出文件名 */
  fileName?: string;
  /** 表头映射关系 */
  headerMap?: Record<string, string>;
  /** 需要导出的字段 */
  fields?: string[];
  /** 导出格式 */
  format: 'excel' | 'csv';
}

/**
 * 数据导出工具类
 */
export class ExportUtil {
  private intl;
  
  constructor() {
    this.intl = useIntl();
  }
  
  /**
   * 处理导出数据
   * @param data 原始数据
   * @param headerMap 表头映射
   * @param fields 需要导出的字段
   * @returns 处理后的导出数据
   */
  private processData(
    data: Record<string, any>[],
    headerMap?: Record<string, string>,
    fields?: string[]
  ): Record<string, any>[] {
    // 如果指定了字段,则只保留指定的字段
    const processedData = fields 
      ? data.map(item => fields.reduce((obj, key) => {
          obj[key] = item[key];
          return obj;
        }, {} as Record<string, any>))
      : [...data];
      
    // 如果有表头映射,则替换键名
    if (headerMap) {
      return processedData.map(item => {
        const newItem: Record<string, any> = {};
        Object.keys(item).forEach(key => {
          const newKey = headerMap[key] || key;
          newItem[newKey] = item[key];
        });
        return newItem;
      });
    }
    
    return processedData;
  }
  
  /**
   * 导出为Excel格式
   * @param options 导出选项
   */
  exportToExcel(options: Omit<ExportOptions, 'format'>) {
    try {
      const { data, fileName = '导出数据', headerMap, fields } = options;
      const processedData = this.processData(data, headerMap, fields);
      
      // 创建工作簿和工作表
      const ws = XLSX.utils.json_to_sheet(processedData);
      const wb = XLSX.utils.book_new();
      XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
      
      // 生成Excel文件并下载
      const excelBuffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
      const blob = new Blob([excelBuffer], { type: 'application/octet-stream' });
      saveAs(blob, `${fileName}.xlsx`);
      
      message.success(this.intl.formatMessage({
        id: 'export.success',
        defaultMessage: '导出成功'
      }));
    } catch (error) {
      console.error('Excel导出失败:', error);
      message.error(this.intl.formatMessage({
        id: 'export.failed',
        defaultMessage: '导出失败,请重试'
      }));
    }
  }
  
  /**
   * 导出为CSV格式
   * @param options 导出选项
   */
  exportToCSV(options: Omit<ExportOptions, 'format'>) {
    try {
      const { data, fileName = '导出数据', headerMap, fields } = options;
      const processedData = this.processData(data, headerMap, fields);
      
      // 转换为CSV字符串
      const header = Object.keys(processedData[0] || {});
      const csvContent = [
        header.join(','),
        ...processedData.map(item => header.map(field => {
          const value = item[field];
          // 处理包含逗号或换行符的字段
          if (typeof value === 'string' && (value.includes(',') || value.includes('\n'))) {
            return `"${value.replace(/"/g, '""')}"`;
          }
          return value;
        }).join(','))
      ].join('\n');
      
      // 生成CSV文件并下载
      const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
      saveAs(blob, `${fileName}.csv`);
      
      message.success(this.intl.formatMessage({
        id: 'export.success',
        defaultMessage: '导出成功'
      }));
    } catch (error) {
      console.error('CSV导出失败:', error);
      message.error(this.intl.formatMessage({
        id: 'export.failed',
        defaultMessage: '导出失败,请重试'
      }));
    }
  }
  
  /**
   * 统一导出入口
   * @param options 导出选项
   */
  export(options: ExportOptions) {
    if (options.format === 'excel') {
      this.exportToExcel(options);
    } else {
      this.exportToCSV(options);
    }
  }
  
  /**
   * 大数据量导出(后端处理)
   * @param apiUrl 导出API地址
   * @param params 导出参数
   * @param fileName 文件名
   */
  async exportLargeData(apiUrl: string, params: Record<string, any>, fileName?: string) {
    try {
      message.loading(this.intl.formatMessage({
        id: 'export.processing',
        defaultMessage: '正在处理数据,请稍候...'
      }), 0);
      
      // 调用后端API获取导出任务ID
      const response = await request(apiUrl, {
        method: 'POST',
        data: params,
      });
      
      const { taskId } = response.data;
      if (!taskId) {
        message.error(this.intl.formatMessage({
          id: 'export.failedGetTaskId',
          defaultMessage: '获取导出任务ID失败'
        }));
        return;
      }
      
      // 轮询检查导出状态
      const checkExportStatus = async () => {
        const statusResponse = await request(`/api/export/status/${taskId}`);
        const { status, fileUrl, message: msg } = statusResponse.data;
        
        if (status === 'completed') {
          message.destroy();
          // 下载文件
          window.open(fileUrl, '_blank');
          message.success(this.intl.formatMessage({
            id: 'export.success',
            defaultMessage: '导出成功'
          }));
        } else if (status === 'failed') {
          message.destroy();
          message.error(msg || this.intl.formatMessage({
            id: 'export.failed',
            defaultMessage: '导出失败,请重试'
          }));
        } else {
          // 继续轮询
          setTimeout(checkExportStatus, 3000);
        }
      };
      
      // 开始轮询
      setTimeout(checkExportStatus, 3000);
    } catch (error) {
      message.destroy();
      console.error('大数据量导出失败:', error);
      message.error(this.intl.formatMessage({
        id: 'export.failed',
        defaultMessage: '导出失败,请重试'
      }));
    }
  }
}

3. 创建导出组件

src/components目录下创建ExportButton组件,用于触发导出功能:

import { Button, Dropdown, Menu, Space, message } from 'antd';
import { DownloadOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import React from 'react';
import { ExportUtil, ExportOptions } from '@/utils/exportUtils';

interface ExportButtonProps {
  /** 导出数据 */
  data: Record<string, any>[];
  /** 表头映射 */
  headerMap?: Record<string, string>;
  /** 需要导出的字段 */
  fields?: string[];
  /** 文件名 */
  fileName?: string;
  /** 大数据量判断阈值 */
  largeDataThreshold?: number;
  /** 大数据量导出API地址 */
  largeDataApiUrl?: string;
  /** 按钮样式 */
  buttonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
}

const ExportButton: React.FC<ExportButtonProps> = ({
  data,
  headerMap,
  fields,
  fileName = '导出数据',
  largeDataThreshold = 1000,
  largeDataApiUrl,
  buttonProps,
}) => {
  const intl = useIntl();
  const exportUtil = new ExportUtil();
  
  // 处理导出操作
  const handleExport = (format: 'excel' | 'csv') => {
    // 判断数据量,决定使用前端导出还是后端导出
    if (data.length > largeDataThreshold && largeDataApiUrl) {
      // 大数据量,使用后端导出
      exportUtil.exportLargeData(largeDataApiUrl, {
        data,
        format,
        fileName,
        headerMap,
        fields,
      });
    } else {
      // 小数据量,前端直接导出
      const options: ExportOptions = {
        data,
        fileName,
        headerMap,
        fields,
        format,
      };
      exportUtil.export(options);
    }
  };
  
  // 导出菜单
  const exportMenu = (
    <Menu>
      <Menu.Item key="excel" onClick={() => handleExport('excel')}>
        <DownloadOutlined />
        {intl.formatMessage({
          id: 'export.excel',
          defaultMessage: '导出Excel'
        })}
      </Menu.Item>
      <Menu.Item key="csv" onClick={() => handleExport('csv')}>
        <DownloadOutlined />
        {intl.formatMessage({
          id: 'export.csv',
          defaultMessage: '导出CSV'
        })}
      </Menu.Item>
    </Menu>
  );
  
  return (
    <Dropdown overlay={exportMenu}>
      <Button {...buttonProps} icon={<DownloadOutlined />}>
        {intl.formatMessage({
          id: 'export.exportData',
          defaultMessage: '导出数据'
        })}
      </Button>
    </Dropdown>
  );
};

export default ExportButton;

4. 在表格页面中使用导出按钮

src/pages/table-list/index.tsx为例,集成导出按钮:

import type {
  ActionType,
  ProColumns,
  ProDescriptionsItemProps,
} from '@ant-design/pro-components';
import {
  FooterToolbar,
  PageContainer,
  ProDescriptions,
  ProTable,
} from '@ant-design/pro-components';
import { FormattedMessage, useIntl, useRequest } from '@umijs/max';
import { Button, Drawer, Input, message } from 'antd';
import React, { useCallback, useRef, useState } from 'react';
import { removeRule, rule } from '@/services/ant-design-pro/api';
import CreateForm from './components/CreateForm';
import UpdateForm from './components/UpdateForm';
import ExportButton from '@/components/ExportButton'; // 导入导出按钮组件

const TableList: React.FC = () => {
  const actionRef = useRef<ActionType | null>(null);
  const [tableData, setTableData] = useState<API.RuleListItem[]>([]); // 存储表格数据
  const [showDetail, setShowDetail] = useState<boolean>(false);
  const [currentRow, setCurrentRow] = useState<API.RuleListItem>();
  const [selectedRowsState, setSelectedRows] = useState<API.RuleListItem[]>([]);
  const intl = useIntl();
  
  // ... 其他代码保持不变 ...
  
  // 处理表格数据请求,保存数据到state
  const { data: tableListData, loading } = useRequest(rule, {
    onSuccess: (data) => {
      setTableData(data.list || []);
    },
  });
  
  // 表头映射关系,用于导出时的字段重命名
  const headerMap = {
    name: intl.formatMessage({
      id: 'pages.searchTable.updateForm.ruleName.nameLabel',
      defaultMessage: '规则名称'
    }),
    desc: intl.formatMessage({
      id: 'pages.searchTable.titleDesc',
      defaultMessage: '描述'
    }),
    callNo: intl.formatMessage({
      id: 'pages.searchTable.titleCallNo',
      defaultMessage: '服务调用次数'
    }),
    status: intl.formatMessage({
      id: 'pages.searchTable.titleStatus',
      defaultMessage: '状态'
    }),
    updatedAt: intl.formatMessage({
      id: 'pages.searchTable.titleUpdatedAt',
      defaultMessage: '最后调度时间'
    }),
  };
  
  return (
    <PageContainer>
      {/* ... 其他代码保持不变 ... */}
      <ProTable<API.RuleListItem, API.PageParams>
        headerTitle={intl.formatMessage({
          id: 'pages.searchTable.title',
          defaultMessage: '查询表格',
        })}
        actionRef={actionRef}
        rowKey="key"
        search={{
          labelWidth: 120,
        }}
        toolBarRender={() => [
          <CreateForm key="create" reload={actionRef.current?.reload} />,
          {/* 添加导出按钮 */}
          <ExportButton
            key="export"
            data={selectedRowsState.length > 0 ? selectedRowsState : tableData}
            headerMap={headerMap}
            fileName="规则列表"
            largeDataApiUrl="/api/export/rule-list"
            buttonProps={{ type: 'primary', style: { marginLeft: 8 } }}
          />,
        ]}
        request={rule}
        columns={columns}
        rowSelection={{
          onChange: (_, selectedRows) => {
            setSelectedRows(selectedRows);
          },
        }}
      />
      {/* ... 其他代码保持不变 ... */}
    </PageContainer>
  );
};

export default TableList;

5. 添加国际化支持

在国际化文件中添加导出相关的翻译文本,以中文为例(src/locales/zh-CN.ts):

export default {
  // ... 其他翻译 ...
  'export.excel': '导出Excel',
  'export.csv': '导出CSV',
  'export.success': '导出成功',
  'export.failed': '导出失败,请重试',
  'export.processing': '正在处理数据,请稍候...',
  'export.failedGetTaskId': '获取导出任务ID失败',
  'export.exportData': '导出数据',
};

6. 后端API实现(Node.js/Express示例)

如果需要支持大数据量导出,后端需要提供相应的API接口。以下是使用Node.js和Express实现的简单示例:

// 导出控制器
const exportController = {
  // 创建导出任务
  createExportTask: async (req, res) => {
    try {
      const { data, format, fileName, headerMap, fields } = req.body;
      
      // 生成任务ID
      const taskId = uuidv4();
      
      // 异步处理导出任务
      exportService.processExport({
        taskId,
        data,
        format,
        fileName,
        headerMap,
        fields,
      });
      
      // 返回任务ID
      res.json({
        success: true,
        data: { taskId }
      });
    } catch (error) {
      console.error('创建导出任务失败:', error);
      res.status(500).json({
        success: false,
        message: '创建导出任务失败'
      });
    }
  },
  
  // 查询导出任务状态
  getExportStatus: async (req, res) => {
    try {
      const { taskId } = req.params;
      
      // 查询任务状态
      const task = await exportService.getExportTask(taskId);
      
      if (!task) {
        return res.json({
          success: false,
          message: '导出任务不存在'
        });
      }
      
      res.json({
        success: true,
        data: {
          status: task.status,
          fileUrl: task.status === 'completed' ? task.fileUrl : null,
          message: task.message
        }
      });
    } catch (error) {
      console.error('查询导出任务状态失败:', error);
      res.status(500).json({
        success: false,
        message: '查询导出任务状态失败'
      });
    }
  }
};

// 导出服务
const exportService = {
  // 存储导出任务状态
  exportTasks: new Map(),
  
  // 处理导出任务
  processExport: async (options) => {
    const { taskId, data, format, fileName, headerMap, fields } = options;
    
    // 初始化任务状态
    this.exportTasks.set(taskId, {
      status: 'processing',
      progress: 0,
      message: '正在处理'
    });
    
    try {
      // 模拟处理延迟
      await new Promise(resolve => setTimeout(resolve, 5000));
      
      // 处理数据
      let processedData = data;
      
      // 按字段筛选
      if (fields && fields.length > 0) {
        processedData = data.map(item => {
          const newItem = {};
          fields.forEach(field => {
            newItem[field] = item[field];
          });
          return newItem;
        });
      }
      
      // 重命名字段
      if (headerMap) {
        processedData = processedData.map(item => {
          const newItem = {};
          Object.keys(item).forEach(key => {
            const newKey = headerMap[key] || key;
            newItem[newKey] = item[key];
          });
          return newItem;
        });
      }
      
      // 生成文件
      const fileUrl = await this.generateExportFile({
        data: processedData,
        format,
        fileName,
        taskId
      });
      
      // 更新任务状态
      this.exportTasks.set(taskId, {
        status: 'completed',
        progress: 100,
        fileUrl,
        message: '导出完成'
      });
      
      // 设置任务过期时间(24小时后删除)
      setTimeout(() => {
        this.exportTasks.delete(taskId);
        // 删除文件
        fs.unlinkSync(path.join(exportDir, `${taskId}.${format}`));
      }, 24 * 60 * 60 * 1000);
      
    } catch (error) {
      console.error('处理导出任务失败:', error);
      this.exportTasks.set(taskId, {
        status: 'failed',
        progress: 0,
        message: '导出失败: ' + error.message
      });
    }
  },
  
  // 生成导出文件
  generateExportFile: async (options) => {
    const { data, format, fileName, taskId } = options;
    
    // 确保导出目录存在
    if (!fs.existsSync(exportDir)) {
      fs.mkdirSync(exportDir, { recursive: true });
    }
    
    // 生成文件路径
    const filePath = path.join(exportDir, `${taskId}.${format}`);
    
    if (format === 'excel') {
      // 生成Excel文件
      const workbook = XLSX.utils.book_new();
      const worksheet = XLSX.utils.json_to_sheet(data);
      XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
      XLSX.writeFile(workbook, filePath);
    } else {
      // 生成CSV文件
      const header = Object.keys(data[0] || {});
      const csvContent = [
        header.join(','),
        ...data.map(item => header.map(field => {
          const value = item[field];
          if (typeof value === 'string' && (value.includes(',') || value.includes('\n'))) {
            return `"${value.replace(/"/g, '""')}"`;
          }
          return value;
        }).join(','))
      ].join('\n');
      
      fs.writeFileSync(filePath, csvContent, 'utf-8');
    }
    
    // 返回文件URL
    return `${exportBaseUrl}/${taskId}.${format}`;
  },
  
  // 获取导出任务
  getExportTask: (taskId) => {
    return this.exportTasks.get(taskId);
  }
};

高级功能实现

1. 自定义导出样式

通过xlsx库提供的功能,我们可以自定义Excel导出的样式:

// 在exportUtils.ts中添加自定义样式功能
import XLSXStyle from 'xlsx-style';

// 添加带样式的Excel导出方法
exportToStyledExcel(options: Omit<ExportOptions, 'format'> & { styleOptions?: any }) {
  try {
    const { data, fileName = '导出数据', headerMap, fields, styleOptions } = options;
    const processedData = this.processData(data, headerMap, fields);
    
    // 创建工作簿和工作表
    const ws = XLSX.utils.json_to_sheet(processedData);
    const wb = XLSX.utils.book_new();
    XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
    
    // 添加样式
    if (styleOptions) {
      // 设置表头样式
      const headerStyle = styleOptions.headerStyle || {
        font: { bold: true },
        alignment: { horizontal: 'center' },
        fill: { fgColor: { rgb: 'CCCCCC' } }
      };
      
      // 设置单元格样式
      const cellStyle = styleOptions.cellStyle || {
        alignment: { vertical: 'center' }
      };
      
      // 应用样式
      const range = XLSX.utils.decode_range(ws['!ref'] || 'A1:A1');
      for (let C = range.s.c; C <= range.e.c; C++) {
        const address = XLSX.utils.encode_col(C) + '1';
        if (!ws[address]) continue;
        ws[address].s = headerStyle;
      }
      
      // 应用单元格样式
      for (let R = range.s.r + 1; R <= range.e.r; R++) {
        for (let C = range.s.c; C <= range.e.c; C++) {
          const address = XLSX.utils.encode_cell({ r: R, c: C });
          if (!ws[address]) continue;
          ws[address].s = cellStyle;
        }
      }
    }
    
    // 生成Excel文件并下载
    const excelBuffer = XLSXStyle.write(wb, { bookType: 'xlsx', type: 'array' });
    const blob = new Blob([excelBuffer], { type: 'application/octet-stream' });
    saveAs(blob, `${fileName}.xlsx`);
    
    message.success('导出成功');
  } catch (error) {
    console.error('Excel导出失败:', error);
    message.error('导出失败,请重试');
  }
}

2. 导出权限控制

在导出功能中添加权限控制,确保只有有权限的用户才能导出数据:

// 在ExportButton组件中添加权限控制
import { useAccess } from '@umijs/max';

const ExportButton: React.FC<ExportButtonProps> = ({
  // ... props
}) => {
  const access = useAccess();
  const intl = useIntl();
  
  // 检查是否有权限导出
  if (!access.canExport) {
    return null; // 没有权限,不显示导出按钮
  }
  
  // ... 组件实现
};

在权限定义文件(src/access.ts)中添加导出权限:

export default function access(initialState: { currentUser?: API.CurrentUser }) {
  const { currentUser } = initialState || {};
  
  return {
    // ... 其他权限
    canExport: currentUser && currentUser.permissions?.includes('export'),
  };
}

3. 导出进度显示

对于大数据量导出,可以添加进度显示功能:

// 修改exportUtils.ts中的exportLargeData方法,添加进度显示
exportLargeData(apiUrl: string, params: any) {
  try {
    const [messageApi, contextHolder] = message.useMessage();
    const progressMessage = messageApi.loading('正在准备导出数据...', 0);
    let progress = 0;
    const progressInterval = setInterval(() => {
      progress = Math.min(progress + Math.random() * 10, 95);
      progressMessage.then(() => {
        messageApi.loading(`正在处理数据,已完成${progress.toFixed(0)}%`, 0);
      });
    }, 1000);
    
    // 调用后端API获取导出任务ID
    request(apiUrl, {
      method: 'POST',
      data: params,
    }).then(response => {
      const { taskId } = response.data;
      if (!taskId) {
        clearInterval(progressInterval);
        progressMessage.then(() => messageApi.error('获取导出任务ID失败'));
        return;
      }
      
      // 轮询检查导出状态
      const checkExportStatus = async () => {
        const statusResponse = await request(`/api/export/status/${taskId}`);
        const { status, progress: taskProgress, fileUrl, message: msg } = statusResponse.data;
        
        // 更新进度
        if (taskProgress !== undefined) {
          progress = taskProgress;
          progressMessage.then(() => {
            messageApi.loading(`正在处理数据,已完成${progress.toFixed(0)}%`, 0);
          });
        }
        
        if (status === 'completed') {
          clearInterval(progressInterval);
          progressMessage.then(() => messageApi.destroy());
          window.open(fileUrl, '_blank');
          messageApi.success('导出成功');
        } else if (status === 'failed') {
          clearInterval(progressInterval);
          progressMessage.then(() => messageApi.destroy());
          messageApi.error(msg || '导出失败,请重试');
        } else {
          setTimeout(checkExportStatus, 3000);
        }
      };
      
      setTimeout(checkExportStatus, 3000);
    }).catch(error => {
      clearInterval(progressInterval);
      progressMessage.then(() => messageApi.destroy());
      console.error('大数据量导出失败:', error);
      messageApi.error('导出失败,请重试');
    });
    
    return contextHolder;
  } catch (error) {
    console.error('大数据量导出失败:', error);
    message.error('导出失败,请重试');
  }
}

性能优化策略

大数据量处理优化

  1. 数据分片处理:对于超大数据集,采用分片处理策略,避免内存溢出
  2. Web Worker:使用Web Worker在后台处理数据,避免阻塞主线程
  3. 流式下载:实现数据流式处理和下载,减少内存占用
// 使用Web Worker处理大数据导出的示例
// src/workers/exportWorker.ts
import XLSX from 'xlsx';

self.onmessage = (e) => {
  const { data, format, fileName, headerMap, fields, chunkSize = 1000 } = e.data;
  
  try {
    // 处理表头映射
    let processedData = data;
    if (fields) {
      processedData = data.map(item => fields.reduce((obj, key) => {
        obj[key] = item[key];
        return obj;
      }, {}));
    }
    
    if (headerMap) {
      processedData = processedData.map(item => {
        const newItem = {};
        Object.keys(item).forEach(key => {
          const newKey = headerMap[key] || key;
          newItem[newKey] = item[key];
        });
        return newItem;
      });
    }
    
    // 分片处理数据
    const totalChunks = Math.ceil(processedData.length / chunkSize);
    let currentChunk = 0;
    
    const workbook = XLSX.utils.book_new();
    let worksheet = XLSX.utils.json_to_sheet([]);
    
    while (currentChunk < totalChunks) {
      const start = currentChunk * chunkSize;
      const end = Math.min((currentChunk + 1) * chunkSize, processedData.length);
      const chunkData = processedData.slice(start, end);
      
      // 将分片数据添加到工作表
      const newSheet = XLSX.utils.json_to_sheet(chunkData);
      worksheet['!ref'] = XLSX.utils.decode_range(worksheet['!ref'] || 'A1:A1');
      const newRange = XLSX.utils.decode_range(newSheet['!ref'] || 'A1:A1');
      
      // 调整新数据的行索引
      for (let R = newRange.s.r; R <= newRange.e.r; R++) {
        for (let C = newRange.s.c; C <= newRange.e.c; C++) {
          const oldAddress = XLSX.utils.encode_cell({ r: R, c: C });
          if (!newSheet[oldAddress]) continue;
          
          // 计算新行索引(加上已有数据的行数)
          const newRow = worksheet['!ref'].e.r + (R - newRange.s.r) + 1;
          const newAddress = XLSX.utils.encode_cell({ r: newRow, c: C });
          
          worksheet[newAddress] = newSheet[oldAddress];
        }
      }
      
      // 更新工作表范围
      worksheet['!ref'] = XLSX.utils.encode_range({
        s: { r: 0, c: 0 },
        e: { r: worksheet['!ref'].e.r + newRange.e.r - newRange.s.r + 1, c: newRange.e.c }
      });
      
      currentChunk++;
      
      // 发送进度更新
      self.postMessage({
        type: 'progress',
        progress: Math.floor((currentChunk / totalChunks) * 100)
      });
    }
    
    XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
    
    // 生成文件
    const excelBuffer = XLSX.write(workbook, { bookType: format === 'excel' ? 'xlsx' : 'csv', type: 'array' });
    
    self.postMessage({
      type: 'complete',
      buffer: excelBuffer,
      format,
      fileName
    }, [excelBuffer.buffer]);
    
  } catch (error) {
    self.postMessage({
      type: 'error',
      message: error.message
    });
  }
};

// 在exportUtils.ts中使用Web Worker
exportToLargeDataInWorker(options: ExportOptions) {
  const [messageApi, contextHolder] = message.useMessage();
  
  // 创建Web Worker
  const worker = new Worker(new URL('../workers/exportWorker.ts', import.meta.url));
  
  // 显示进度
  const progressMessage = messageApi.loading('准备导出数据...', 0);
  
  // 监听Worker消息
  worker.onmessage = (e) => {
    switch (e.data.type) {
      case 'progress':
        progressMessage.then(() => {
          messageApi.loading(`正在导出数据,已完成${e.data.progress}%`, undefined);
        });
        break;
      case 'complete':
        progressMessage.then(() => messageApi.destroy());
        // 创建Blob并下载
        const blob = new Blob([e.data.buffer], { 
          type: e.data.format === 'excel' ? 
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' : 
            'text/csv;charset=utf-8;' 
        });
        saveAs(blob, `${e.data.fileName}.${e.data.format === 'excel' ? 'xlsx' : 'csv'}`);
        messageApi.success('导出成功');
        worker.terminate();
        break;
      case 'error':
        progressMessage.then(() => messageApi.destroy());
        messageApi.error('导出失败: ' + e.data.message);
        worker.terminate();
        break;
    }
  };
  
  // 发送数据到Worker
  worker.postMessage(options);
  
  return contextHolder;
}

常见问题与解决方案

1. 中文乱码问题

问题描述:导出CSV文件时,用Excel打开出现中文乱码。

解决方案:在生成CSV文件时添加BOM头:

// 修改CSV导出代码
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });

2. 大数据导出性能问题

问题描述:导出大量数据时,前端页面卡顿或崩溃。

解决方案

  • 实现数据分片处理
  • 使用Web Worker在后台处理数据
  • 超过一定数据量时,改用后端导出

3. 导出文件过大问题

问题描述:导出大量数据时,生成的Excel文件过大。

解决方案

  • 仅导出必要字段
  • 对长文本进行截断处理
  • 拆分多个工作表
  • 考虑使用CSV格式代替Excel

4. 日期格式问题

问题描述:导出的日期在Excel中显示为数字而非日期格式。

解决方案:在导出前格式化日期:

// 在数据处理函数中添加日期格式化
const processData = (data) => {
  return data.map(item => {
    const newItem = { ...item };
    // 格式化日期字段
    if (newItem.updatedAt) {
      newItem.updatedAt = dayjs(newItem.updatedAt).format('YYYY-MM-DD HH:mm:ss');
    }
    return newItem;
  });
};

总结与展望

本文详细介绍了在Ant Design Pro项目中实现Excel/CSV数据导出功能的完整方案,包括技术选型、架构设计、详细实现步骤以及高级功能扩展。通过本文提供的方案,你可以在项目中快速集成专业级的数据导出功能,并根据实际需求进行定制化开发。

未来,我们可以进一步优化导出功能,例如:

  1. 支持更多导出格式(如PDF、JSON等)
  2. 实现导出模板自定义功能
  3. 添加数据可视化导出(图表导出)
  4. 支持增量导出和定时导出

希望本文能够帮助你解决Ant Design Pro项目中的数据导出问题,提升用户体验和系统功能完整性。如果你有任何问题或建议,欢迎在评论区留言讨论。

如果你觉得本文对你有帮助,请点赞、收藏并关注,以便获取更多前端开发实战教程!

【免费下载链接】ant-design-pro 👨🏻‍💻👩🏻‍💻 Use Ant Design like a Pro! 【免费下载链接】ant-design-pro 项目地址: https://gitcode.com/gh_mirrors/an/ant-design-pro

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值