UniApp 安卓端实现多Sheet Excel文件导出的完整指南
·
前言: 在移动应用开发中,经常需要将数据导出为Excel格式,特别是包含多个Sheet的数据报表。UniApp作为跨平台开发框架,在实现Excel文件下载功能时会遇到一些挑战。本文将详细介绍如何在UniApp安卓端实现下载包含多个Sheet的Excel文件。
技术选型: 考虑到UniApp安卓端的特性,我们采用前端生成Excel文件的方案,使用xlsx库实现多Sheet导出。
一、环境准备与配置
1. 安装必要依赖:xlsx库
npm install xlsx
2. manifest.json 配置
{
"app-plus": {
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />",
"<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />"
]
}
}
}
}
二、核心实现步骤
1. 创建公共文件
1.1 引入方法与定义变量
// utils/exportToExcel.js
import * as XLSX from 'xlsx';
import {
toast
} from './util.js';
import moment from 'moment';
// 默认列配置(展示部分)
const DEFAULT_COLUMNS = [{
key: 'snCode',
label: '目标ID',
width: 20
},
{
key: 'model',
label: '机型',
width: 10
},
...
];
1.2 导出主方法
/**
* 导出Excel文件到本地
* @param {Array} dataGroups 分组数据数组(一维数组或者二维数组)
* @param {string} fileName 文件名(不含扩展名)
* @returns {Promise<boolean>} 导出是否成功
*/
export const exportToExcel = async (dataGroups, fileName = '数据', isAutoOpen = false) => {
try {
// 验证数据
if (!Array.isArray(dataGroups)) {
return {
status: false,
msg: "下载数据格式错误"
};
}
let validGroups = [];
// 判断数据格式并处理
if (dataGroups.length === 0) {
return {
status: false,
msg: "无数据可下载"
};
} else if (Array.isArray(dataGroups[0])) {
// 二维数组,直接使用
validGroups = dataGroups.filter(group =>
Array.isArray(group) && group.length
);
} else {
// 一维数组,需要分组
validGroups = groupDataBySnCode(dataGroups).filter(group =>
group.length
);
}
// 确保每组数据都有snCode
validGroups = validGroups.filter(group =>
group.some(item => item.snCode)
);
if (validGroups.length === 0) {
return {
status: false,
msg: "无有效数据可下载"
};
}
// 创建工作簿
const wb = await createWorkbookWithSheets(validGroups);
if (!wb.SheetNames || wb.SheetNames.length === 0) {
return {
status: false,
msg: "创建工作簿失败,请检查数据格式"
};
}
// 生成Excel二进制数据
const wbout = XLSX.write(wb, {
bookType: 'xlsx',
type: 'array', // 输出Uint8Array
bookSST: false,
compression: true // 启用压缩,避免文件损坏
});
const fullFileName = `${fileName}.xlsx`;
// 保存文件到本地
const success = await saveFile(wbout, fullFileName,
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', isAutoOpen);
if (success) {
return {
status: true,
msg: `文件已保存至${fullFileName}`
};
} else {
return {
status: false,
msg: "请检查权限设置"
};
}
} catch (error) {
console.error('下载Excel失败:', error);
return {
status: false,
msg: error.message
};
}
};
1.3 数据类方法
// 处理数据,转换时间戳等字段
const processData = (data) => {
if (!Array.isArray(data)) return [];
return data.map(item => {
const finnalHeight = item.height ? Number(item.height).toFixed(2) : 0;
const finnalAltitude = item.altitude ? Number(item.altitude).toFixed(2) : 0;
// 重要:表格列对应的字段都要return(这里展示部分)
return {
snCode: item.snCode || '',
longitude: item.longitude ? Number(item.longitude).toFixed(6) : 0,
latitude: item.latitude ? Number(item.latitude).toFixed(6) : 0,
height: `${finnalHeight}米 [${finnalAltitude}米]`,
...
};
});
};
/**
* 按snCode分组数据
* @param {Array} flatData 一维数组
* @returns {Array} 分组后的数据数组(二维数组)
*/
export const groupDataBySnCode = (flatData) => {
if (!Array.isArray(flatData)) {
console.error("groupDataBySnCode: 输入不是数组");
return [];
}
const groups = {};
flatData.forEach((item, index) => {
if (!item || typeof item !== 'object') {
console.warn(`第${index}项数据无效`);
return;
}
const snCode = item.snCode || '未知设备';
if (!groups[snCode]) {
groups[snCode] = [];
}
groups[snCode].push(item);
});
const result = Object.values(groups);
return result;
};
1.4 工具类方法
// 将时间戳转换为可读的日期时间字符串
const formatTimestamp = (timestamp) => {
if (!timestamp || timestamp <= 0) return '-';
return moment(timestamp).format('YYYY-MM-DD HH:mm:ss')
};
// 创建自定义的工作表
const createCustomSheet = (data, sheetName) => {
try {
if (!Array.isArray(data) || data.length === 0) {
console.warn(`Sheet ${sheetName} 数据为空`);
return null;
}
// 处理数据
const processedData = processData(data);
// 提取表头
const headers = DEFAULT_COLUMNS.map(col => col.label);
// 提取数据行
const dataRows = processedData.map(item => {
return DEFAULT_COLUMNS.map(col => {
const value = item[col.key];
// 处理空值
if (value === undefined || value === null) {
return '';
}
// 返回原始值,让XLSX自动处理类型
return value;
});
});
// 合并表头和数据
const wsData = [headers, ...dataRows];
// 创建工作表
const ws = XLSX.utils.aoa_to_sheet(wsData);
// 设置列宽
if (!ws['!cols']) ws['!cols'] = [];
ws['!cols'] = DEFAULT_COLUMNS.map(col => ({
wch: col.width
}));
return ws;
} catch (error) {
console.error(`创建Sheet ${sheetName} 失败:`, error);
return null;
}
};
// 保存文件到设备本地
const saveFile = async (data, fileName, mimeType =
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', isAutoOpen) => {
return new Promise((resolve) => {
if (!plus) {
console.error('plus 对象不存在,无法保存文件');
resolve(false);
return;
}
// 移除所有非法文件名字符
fileName = fileName.replace(/[\\/*?:[\]|<>]/g, '_');
try {
// 将 ArrayBuffer 转换为 Base64
let base64Data;
if (data instanceof ArrayBuffer) {
console.log('数据是ArrayBuffer,转换为Base64');
const bytes = new Uint8Array(data);
let binary = '';
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
base64Data = btoa(binary);
} else if (data instanceof Uint8Array) {
console.log('数据是Uint8Array,转换为Base64');
let binary = '';
const len = data.length;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(data[i]);
}
base64Data = btoa(binary);
} else {
console.error('不支持的数据类型:', typeof data);
toast('数据格式错误');
resolve(false);
return;
}
// 获取文档目录
plus.io.requestFileSystem(plus.io.PRIVATE_DOC, (fs) => {
fs.root.getFile(fileName, {
create: true
}, (fileEntry) => {
fileEntry.createWriter((writer) => {
writer.onwriteend = () => {
// 是否自动打开文件
if (isAutoOpen) {
// 获取文件完整路径
const filePath = fileEntry.toURL();
try {
// 打开文件
openFile(filePath);
} catch (openError) {
console.error('文件打开失败:', openError);
// toast('文件已保存,但打开失败: ' + openError
// .message);
}
}
resolve(true);
};
writer.onerror = (e) => {
console.error('文件写入失败:', JSON.stringify(e));
toast('文件保存失败: ' + (e.message || '未知错误'));
resolve(false);
};
try {
// 使用 writeAsBinary 方法写入 Base64 数据
writer.writeAsBinary(base64Data);
} catch (writeError) {
console.error('文件写入异常:', writeError);
toast('文件写入失败');
resolve(false);
}
}, (e) => {
console.error('创建writer失败:', e);
toast('创建文件写入器失败: ' + (e.message || '未知错误'));
resolve(false);
});
}, (e) => {
console.error('获取文件失败:', e);
toast('获取文件失败: ' + (e.message || '未知错误'));
resolve(false);
});
}, (e) => {
console.error('获取文件系统失败:', e);
toast('获取存储系统失败: ' + (e.message || '未知错误'));
resolve(false);
});
} catch (error) {
console.error('保存文件异常:', error);
toast('保存文件异常: ' + error.message);
resolve(false);
}
});
};
/**
* 打开文件
* @param {string} filePath 文件路径
* @param {string} mimeType 文件MIME类型
*/
const openFile = (filePath) => {
if (!filePath) return;
uni.openDocument({
filePath,
fileType: 'xlsx',
success: (res) => {
uni.hideLoading();
console.log('打开文档成功', res);
},
fail: (err) => {
uni.hideLoading();
console.error('打开文档失败', err);
uni.showToast({
title: '无法打开文件,请使用WPS或Excel查看',
icon: 'none',
duration: 3000
});
}
});
}
2. 主页面实现
// 先引入导出方法
import {
exportToExcel
} from "@/utils/exportToExcel.js";
import {
toast // 自定义提示方法,需替换成自己的
} from '@/utils/util.js';
import moment from 'moment';
data() {
return {
isDownloading: false, // 防止重复点击
};
},
methods: {
// 点击下载按钮执行
async handleDownload() {
if (this.isDownloading) return;
// 获取数据:我这里通过SQLite获取的,替换成自己的数据即可
const res = await this.$droneDBHelper.getTrajectoryByBatchIds(
this.checkedIds
);
const resArr = Array.from(res.values()); // 将 Map 转换为数组
let data = [];
if (Array.isArray(resArr) && resArr.length) {
data = resArr;
}
const currentDate = moment().format("YYYY-MM-DD_HH-mm-ss");
const fileName = `轨迹数据_${currentDate}`; // 自定义文件名
this.isDownloading = true;
loading("下载中...");
try {
const isAutoOpen = false;
const res = await exportToExcel(data, fileName, isAutoOpen); // data: 数据,fileName:导出文件名,isAutoOpen:是否自动打开
if (res.status) {
toast("下载成功");
} else {
toast("下载失败:" + res.msg);
}
} catch (error) {
console.error("下载报错:", error);
} finally {
hideLoading();
this.isDownloading = false;
}
},
}
导出的数据格式如下(展示部分):展示的是二维数组,也可是一维数组
[
[{
"snCode": "111",
"frequency": 5776.5,
"model": "",
"latitude": 11.517910008958317,
"longitude": 101.28125619156881,
"altitude": 10,
"direct": 10,
"speed": 333,
"height": 33,
"totalDistance": 120,
"startDate": 1764937404513,
"createDate": 1764937404514,
},
...
],
[{
"snCode": "222",
"frequency": 2437,
"model": "",
"latitude": 21.517910008958317,
"longitude": 101.28125619156881,
"altitude": 220,
"direct": 20,
"speed": 500,
"height": 50,
"totalDistance": 560,
"startDate": 1764937409235,
"createDate": 1764937409236,
},
...
]
]
更多推荐


所有评论(0)