欢迎光临易鼎网
详情描述

xlsx 库实现 Excel 导入导出功能,由于小程序环境限制(无 DOM、File API 等),需要特殊处理。以下是完整实现方案:

一、准备工作

1. 下载并引入 xlsx 库

  • 下载 xlsx.mini.min.js(推荐使用精简版,体积约 1.5MB)
  • 放入小程序项目的 utils 目录
  • app.json 中添加依赖:
{
  "usingComponents": {},
  "requireNativeModule": {
    "fs": "system.fs"
  }
}

二、Excel 导入功能

1. 选择 Excel 文件

import xlsx from '../../utils/xlsx.mini.min.js';

Page({
  data: {
    tableData: []
  },

  // 选择Excel文件
  chooseExcelFile() {
    wx.chooseMessageFile({
      count: 1,
      type: 'file',
      extension: ['xlsx', 'xls'],
      success: (res) => {
        const tempFilePath = res.tempFiles[0].path;
        this.readExcelFile(tempFilePath);
      }
    })
  },

  // 读取Excel文件
  async readExcelFile(filePath) {
    try {
      // 读取文件为 ArrayBuffer
      const arrayBuffer = await this.readFileAsArrayBuffer(filePath);

      // 解析Excel数据
      const workbook = xlsx.read(arrayBuffer, { type: 'array' });

      // 获取第一个工作表
      const firstSheetName = workbook.SheetNames[0];
      const worksheet = workbook.Sheets[firstSheetName];

      // 转换为JSON数据
      const jsonData = xlsx.utils.sheet_to_json(worksheet, {
        header: 1, // 以数组形式返回,包含表头
        defval: '' // 空单元格默认值
      });

      // 处理数据
      this.processExcelData(jsonData);

    } catch (error) {
      console.error('读取Excel失败:', error);
      wx.showToast({
        title: '导入失败',
        icon: 'none'
      });
    }
  },

  // 读取文件为ArrayBuffer
  readFileAsArrayBuffer(filePath) {
    return new Promise((resolve, reject) => {
      wx.getFileSystemManager().readFile({
        filePath: filePath,
        encoding: 'binary', // 重要:使用binary编码
        success: (res) => {
          // 将二进制数据转换为ArrayBuffer
          const buffer = new Uint8Array(res.data);
          resolve(buffer);
        },
        fail: reject
      });
    });
  },

  // 处理Excel数据
  processExcelData(data) {
    if (!data || data.length === 0) return;

    // 提取表头(第一行)
    const headers = data[0];

    // 提取数据行(从第二行开始)
    const rows = data.slice(1).map(row => {
      const obj = {};
      headers.forEach((header, index) => {
        obj[header] = row[index] || '';
      });
      return obj;
    });

    this.setData({
      tableData: rows
    });

    wx.showToast({
      title: `导入成功,共${rows.length}条数据`,
      icon: 'success'
    });
  }
})

三、Excel 导出功能

1. 数据导出为 Excel

// 导出Excel
exportToExcel() {
  const { tableData } = this.data;

  if (!tableData || tableData.length === 0) {
    wx.showToast({
      title: '没有数据可导出',
      icon: 'none'
    });
    return;
  }

  try {
    // 准备工作表数据
    const wsData = this.prepareWorksheetData(tableData);

    // 创建工作表
    const worksheet = xlsx.utils.aoa_to_sheet(wsData);

    // 设置列宽
    worksheet['!cols'] = this.calculateColumnWidths(wsData);

    // 创建工作簿
    const workbook = xlsx.utils.book_new();
    xlsx.utils.book_append_sheet(workbook, worksheet, 'Sheet1');

    // 生成二进制数据
    const excelBuffer = xlsx.write(workbook, {
      bookType: 'xlsx',
      type: 'array'
    });

    // 保存文件
    this.saveExcelFile(excelBuffer);

  } catch (error) {
    console.error('导出Excel失败:', error);
    wx.showToast({
      title: '导出失败',
      icon: 'none'
    });
  }
},

// 准备工作表数据(二维数组)
prepareWorksheetData(data) {
  if (!data.length) return [];

  // 获取表头
  const headers = Object.keys(data[0]);
  const result = [headers]; // 第一行为表头

  // 添加数据行
  data.forEach(item => {
    const row = headers.map(header => item[header] || '');
    result.push(row);
  });

  return result;
},

// 计算列宽
calculateColumnWidths(data) {
  if (!data.length) return [];

  const colWidths = [];
  const maxRows = Math.min(data.length, 100); // 只检查前100行

  // 初始化列宽
  for (let i = 0; i < data[0].length; i++) {
    colWidths.push({ wch: 10 }); // 默认宽度
  }

  // 根据内容调整列宽
  for (let col = 0; col < data[0].length; col++) {
    let maxLength = 0;
    for (let row = 0; row < maxRows; row++) {
      const cellValue = String(data[row][col] || '');
      maxLength = Math.max(maxLength, cellValue.length);
    }
    // 设置宽度,考虑中文字符
    colWidths[col].wch = Math.min(Math.max(maxLength * 1.2, 10), 50);
  }

  return colWidths;
},

// 保存Excel文件
saveExcelFile(buffer) {
  // 转换为base64
  const base64 = wx.arrayBufferToBase64(buffer);

  // 获取文件路径
  const filePath = `${wx.env.USER_DATA_PATH}/export_${Date.now()}.xlsx`;

  // 写入文件
  wx.getFileSystemManager().writeFile({
    filePath: filePath,
    data: base64,
    encoding: 'base64',
    success: () => {
      // 保存到手机
      wx.saveFileToDisk({
        filePath: filePath,
        success: () => {
          wx.showToast({
            title: '导出成功',
            icon: 'success'
          });
        },
        fail: (err) => {
          console.error('保存失败:', err);
          // 备用方案:使用文件管理器打开
          wx.openDocument({
            filePath: filePath,
            showMenu: true
          });
        }
      });
    },
    fail: (err) => {
      console.error('写入文件失败:', err);
      wx.showToast({
        title: '写入文件失败',
        icon: 'none'
      });
    }
  });
}

四、完整页面 WXML 示例

<!-- index.wxml -->
<view class="container">
  <!-- 导入按钮 -->
  <button type="primary" bindtap="chooseExcelFile">
    导入Excel
  </button>

  <!-- 导出按钮 -->
  <button type="warn" bindtap="exportToExcel" wx:if="{{tableData.length > 0}}">
    导出Excel ({{tableData.length}}条)
  </button>

  <!-- 数据展示 -->
  <scroll-view scroll-y class="table-container" wx:if="{{tableData.length > 0}}">
    <view class="table">
      <!-- 表头 -->
      <view class="table-row header">
        <block wx:for="{{Object.keys(tableData[0])}}" wx:key="index">
          <view class="table-cell">{{item}}</view>
        </block>
      </view>

      <!-- 数据行 -->
      <view class="table-row" wx:for="{{tableData}}" wx:key="index">
        <block wx:for="{{Object.values(item)}}" wx:key="index">
          <view class="table-cell">{{item || '-'}}</view>
        </block>
      </view>
    </view>
  </scroll-view>

  <!-- 空状态 -->
  <view class="empty" wx:if="{{tableData.length === 0}}">
    <text>暂无数据,请先导入Excel文件</text>
  </view>
</view>

五、样式文件

/* index.wxss */
.container {
  padding: 20rpx;
}

button {
  margin: 20rpx 0;
}

.table-container {
  height: 800rpx;
  margin-top: 40rpx;
  border: 1rpx solid #ddd;
  border-radius: 8rpx;
}

.table {
  display: table;
  width: 100%;
}

.table-row {
  display: table-row;
}

.table-row.header {
  background-color: #f5f5f5;
  font-weight: bold;
}

.table-cell {
  display: table-cell;
  padding: 20rpx;
  border-bottom: 1rpx solid #eee;
  border-right: 1rpx solid #eee;
  word-break: break-all;
  min-width: 120rpx;
}

.table-row:last-child .table-cell {
  border-bottom: none;
}

.table-cell:last-child {
  border-right: none;
}

.empty {
  text-align: center;
  color: #999;
  margin-top: 100rpx;
}

六、注意事项

1. 文件大小限制

  • 小程序选择文件大小限制为 10MB
  • 建议处理前先检查文件大小

2. 性能优化

// 大型文件分片处理
processLargeExcel(data, chunkSize = 1000) {
  const chunks = [];
  for (let i = 0; i < data.length; i += chunkSize) {
    chunks.push(data.slice(i, i + chunkSize));
  }

  // 分批次处理
  this.processChunks(chunks, 0);
},

processChunks(chunks, index) {
  if (index >= chunks.length) return;

  // 使用setTimeout避免阻塞
  setTimeout(() => {
    const chunk = chunks[index];
    // 处理当前分片
    this.processDataChunk(chunk);

    // 更新进度
    wx.showLoading({
      title: `处理中 ${index + 1}/${chunks.length}`,
    });

    // 处理下一分片
    this.processChunks(chunks, index + 1);
  }, 0);
}

3. 兼容性处理

// 检查xlsx库是否加载
if (typeof xlsx === 'undefined') {
  console.error('xlsx库未加载');
  return;
}

// 检查文件类型
function checkFileType(filePath) {
  const ext = filePath.split('.').pop().toLowerCase();
  return ['xlsx', 'xls', 'csv'].includes(ext);
}

七、替代方案(如果xlsx库太大)

如果 xlsx.mini.min.js 体积仍然过大,可以考虑:

使用云函数处理:将文件上传到云函数,在云端处理Excel 使用简化版本:只导入需要的功能模块 使用 Web Worker:在小程序中使用 Worker 线程处理

这个方案应该能覆盖小程序中 Excel 导入导出的基本需求。根据具体业务场景,你可能需要调整数据处理的逻辑。