xlsx 库实现 Excel 导入导出功能,由于小程序环境限制(无 DOM、File API 等),需要特殊处理。以下是完整实现方案:
xlsx.mini.min.js(推荐使用精简版,体积约 1.5MB)utils 目录app.json 中添加依赖:{
"usingComponents": {},
"requireNativeModule": {
"fs": "system.fs"
}
}
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
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'
});
}
});
}
<!-- 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;
}
// 大型文件分片处理
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);
}
// 检查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.mini.min.js 体积仍然过大,可以考虑:
这个方案应该能覆盖小程序中 Excel 导入导出的基本需求。根据具体业务场景,你可能需要调整数据处理的逻辑。