前阵子在和朋友讨论,记账类的app已经那么普遍了,如果想要做点不一样的还能加什么功能。从我自己的角度出发,我觉得完全可以加一个导出Excel嘛。但是目前原生鸿蒙并没有导出Excel的方法,常规的做法是通过服务器端生成Excel文件后,再下载到本地......服务器端的方案肯定是不成的,因为大家都知道,记账属于独立开发三件套止一,独立开发意味着......哪里来的钱租服务器啊。
所以,我们还是自己做一个Excel导出的三方库吧
首先要思考一下原理:excel并不像html网页或者txt文件一样,可以通过直接编辑内容指定一个文件格式来生成文件,如果我们对一个excel文件右键通过记事本打开的话,你会发现一堆乱码,不过这个思路还是可以用的,比如说csv格式的数据然后保存成xls格式,但是csv只能上数据不能做样式,还好,还有一个xml可以用,因为从Excel 2003开始,就引入了xml格式,而且还可以支持样式。
接下来思路就简单了,大致来说就是:定义一个数据格式,要包含文件名、每行每列的内容还可以加入样式,然后根据xml的文件格式去生成一个完整的文件内容,最后通过@ohos.file.fs (文件管理)接口,来创建一个文件,写入内容,再保存。保存到本地用的是@ohos.file.picker (选择器)中的DocumentViewPicker,即让用户选择一个文件保存的地址。
已经上传到了OpenHarmony的三方库中,具体使用办法如下:
安装使用
基础安装
ohpm install excel_hm
1. 导入库文件
typescript
import {
ExcelGenerator,
ExcelTableData,
ExcelCellData,
ExcelGenerateOptions,
ExcelGenerateResult
} from 'excel_hm';
2. 创建生成器实例
typescript
const context = getContext(this) as common.UIAbilityContext;
const excelGenerator = new ExcelGenerator(context);
数据格式规范
ExcelTableData 接口
typescript
interface ExcelTableData {
title: string; // 表格标题(必填)
headers?: string[]; // 表头数组(可选)
data: ExcelCellData[][]; // 二维数据数组(必填)
titleStyle?: ExcelCellStyle; // 标题样式(可选)
headerStyle?: ExcelCellStyle; // 表头样式(可选)
dataStyle?: ExcelCellStyle; // 数据样式(可选)
}
ExcelCellData 接口
typescript
interface ExcelCellData {
value: string; // 单元格内容(必填)
style?: ExcelCellStyle; // 单元格样式(可选)
}
ExcelCellStyle 接口
typescript
interface ExcelCellStyle {
fontWeight?: 'normal' | 'bold'; // 字体粗细
fontSize?: number; // 字体大小
fontColor?: string; // 字体颜色
backgroundColor?: string; // 背景颜色
alignment?: 'left' | 'center' | 'right'; // 对齐方式
}
使用示例
基础用法
typescript
// 1. 创建简单表格数据
const tableData = ExcelGenerator.createSimpleTableData(
'员工信息表',
['姓名', '年龄', '部门'],
[
['张三', '25', '技术部'],
['李四', '30', '销售部'],
['王五', '28', '人事部']
]
);
// 2. 生成Excel文件
const result = await excelGenerator.generateExcel(tableData);
if (result.success) {
console.log('生成成功:'+ result.message);
// 3. 保存到本地
const saveResult = await excelGenerator.saveToLocal(result.filePath!, result.fileName!);
console.log('保存结果:'+ saveResult.message);
} else {
console.error('生成失败:'+ result.message);
}
完整Demo代码
typescript
import { common } from '@kit.AbilityKit';
import router from '@ohos.router';
import {
ExcelGenerator,
ExcelTableData,
ExcelCellData,
ExcelGenerateOptions,
ExcelGenerateResult
} from 'excel_hm';
@Entry
@Component
struct Excel {
@State tableTitle: string = '员工信息表';
@State rows: number = 5;
@State cols: number = 3;
@State tableData: ExcelCellData[][] = [];
@State isGenerating: boolean = false;
@State message: string = '';
@State messageType: 'success' | 'error' | '' = '';
@State showFileActions: boolean = false;
private excelGenerator?: ExcelGenerator;
aboutToAppear() {
// 初始化Excel生成器
const context = getContext(this) as common.UIAbilityContext;
this.excelGenerator = new ExcelGenerator(context);
this.initializeTable();
}
// 初始化表格数据
initializeTable() {
this.tableData = [];
// 默认测试数据
const testData = [
['姓名', '年龄', '部门'],
['张三', '25', '技术部'],
['李四', '30', '销售部'],
['王五', '28', '人事部'],
['赵六', '32', '财务部']
];
for (let i = 0; i < this.rows; i++) {
const row: ExcelCellData[] = [];
for (let j = 0; j < this.cols; j++) {
// 如果有测试数据且在范围内,使用测试数据,否则为空
const testValue = (i < testData.length && j < testData[i].length) ? testData[i][j] : '';
row.push({ value: testValue });
}
this.tableData.push(row);
}
}
build() {
Column() {
// 标题栏
Row() {
Text('Excel生成器')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#8B4513')
Blank()
}
.width('100%')
.padding({ left: 20, right: 20, top: 15, bottom: 15 })
.backgroundColor('#F5F5F5')
// 主要内容区域
Scroll() {
Column({ space: 20 }) {
// 表格配置卡片
this.buildConfigCard()
// 表格标题卡片
this.buildTitleCard()
// 表格编辑卡片
this.buildTableCard()
// 生成按钮卡片
this.buildGenerateCard()
// 消息提示
if (this.message) {
this.buildMessageCard()
}
// 文件操作卡片
if (this.showFileActions) {
this.buildFileActionsCard()
}
}
.padding({ left: 20, right: 20, top: 20, bottom: 30 })
.width('100%')
}
.layoutWeight(1)
.backgroundColor('#F8F8F8')
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Auto)
}
.width('100%')
.height('100%')
.backgroundColor('#F8F8F8')
}
@Builder
buildConfigCard() {
Column({ space: 15 }) {
Row({ space: 10 }) {
Text('⚙️')
.fontSize(24)
Text('表格配置')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#8B4513')
}
.alignItems(VerticalAlign.Center)
Row({ space: 20 }) {
Column({ space: 8 }) {
Text('行数')
.fontSize(14)
.fontColor('#666666')
Row({ space: 10 }) {
Button('-')
.width(40)
.height(40)
.fontSize(18)
.backgroundColor('#F0F0F0')
.fontColor('#666666')
.borderRadius(8)
.enabled(this.rows > 1)
.onClick(() => {
if (this.rows > 1) {
this.rows--;
this.initializeTable();
}
})
Text(this.rows.toString())
.fontSize(16)
.fontWeight(FontWeight.Medium)
.width(40)
.textAlign(TextAlign.Center)
Button('+')
.width(40)
.height(40)
.fontSize(18)
.backgroundColor('#8B4513')
.fontColor('#FFFFFF')
.borderRadius(8)
.enabled(this.rows < 10)
.onClick(() => {
if (this.rows < 10) {
this.rows++;
this.initializeTable();
}
})
}
}
.layoutWeight(1)
Column({ space: 8 }) {
Text('列数')
.fontSize(14)
.fontColor('#666666')
Row({ space: 10 }) {
Button('-')
.width(40)
.height(40)
.fontSize(18)
.backgroundColor('#F0F0F0')
.fontColor('#666666')
.borderRadius(8)
.enabled(this.cols > 1)
.onClick(() => {
if (this.cols > 1) {
this.cols--;
this.initializeTable();
}
})
Text(this.cols.toString())
.fontSize(16)
.fontWeight(FontWeight.Medium)
.width(40)
.textAlign(TextAlign.Center)
Button('+')
.width(40)
.height(40)
.fontSize(18)
.backgroundColor('#8B4513')
.fontColor('#FFFFFF')
.borderRadius(8)
.enabled(this.cols < 10)
.onClick(() => {
if (this.cols < 10) {
this.cols++;
this.initializeTable();
}
})
}
}
.layoutWeight(1)
}
.width('100%')
}
.width('100%')
.padding(20)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({
radius: 8,
color: '#10000000',
offsetX: 0,
offsetY: 2
})
}
@Builder
buildTitleCard() {
Column({ space: 15 }) {
Row({ space: 10 }) {
Text('📝')
.fontSize(24)
Text('表格标题')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#8B4513')
}
.alignItems(VerticalAlign.Center)
TextInput({
text: this.tableTitle,
placeholder: '请输入表格标题...'
})
.fontSize(16)
.backgroundColor('#F8F8F8')
.borderRadius(8)
.padding({ left: 15, right: 15 })
.border({
width: 1,
color: '#E0E0E0'
})
.onChange((value: string) => {
this.tableTitle = value;
})
}
.width('100%')
.padding(20)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({
radius: 8,
color: '#10000000',
offsetX: 0,
offsetY: 2
})
}
@Builder
buildTableCard() {
Column({ space: 15 }) {
Row({ space: 10 }) {
Text('📊')
.fontSize(24)
Text('表格内容')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#8B4513')
}
.alignItems(VerticalAlign.Center)
// 表格编辑区域
Scroll() {
Column({ space: 2 }) {
ForEach(this.tableData, (row: ExcelCellData[], rowIndex: number) => {
Row({ space: 2 }) {
ForEach(row, (cell: ExcelCellData, colIndex: number) => {
TextInput({
text: cell.value,
placeholder: `R${rowIndex + 1}C${colIndex + 1}`
})
.fontSize(12)
.backgroundColor('#F8F8F8')
.borderRadius(4)
.padding({ left: 8, right: 8 })
.border({
width: 1,
color: '#E0E0E0'
})
.layoutWeight(1)
.height(40)
.onChange((value: string) => {
this.tableData[rowIndex][colIndex].value = value;
})
})
}
.width('100%')
})
}
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(8)
.padding(10)
.border({
width: 1,
color: '#E0E0E0'
})
Text(`当前表格大小:${this.rows} 行 × ${this.cols} 列`)
.fontSize(12)
.fontColor('#999999')
}
.width('100%')
.padding(20)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({
radius: 8,
color: '#10000000',
offsetX: 0,
offsetY: 2
})
}
@Builder
buildGenerateCard() {
Column({ space: 15 }) {
Row({ space: 10 }) {
Text('💾')
.fontSize(24)
Text('生成Excel')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#8B4513')
}
.alignItems(VerticalAlign.Center)
Text('点击下方按钮生成Excel文件,将自动弹出保存对话框')
.fontSize(14)
.fontColor('#666666')
Button() {
Row({ space: 10 }) {
if (this.isGenerating) {
LoadingProgress()
.width(20)
.height(20)
.color('#FFFFFF')
} else {
Text('📄')
.fontSize(20)
}
Text(this.isGenerating ? '生成中...' : '生成Excel文件')
.fontSize(16)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Medium)
}
}
.width('100%')
.height(50)
.backgroundColor('#8B4513')
.borderRadius(12)
.enabled(!this.isGenerating && this.tableTitle.trim() !== '')
.onClick(() => {
this.generateExcel();
})
if (this.tableTitle.trim() === '') {
Text('请先输入表格标题')
.fontSize(12)
.fontColor('#FF6B6B')
}
}
.width('100%')
.padding(20)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({
radius: 8,
color: '#10000000',
offsetX: 0,
offsetY: 2
})
}
@Builder
buildMessageCard() {
Row({ space: 10 }) {
Text(this.messageType === 'success' ? '✅' : '❌')
.fontSize(20)
Text(this.message)
.fontSize(14)
.fontColor(this.messageType === 'success' ? '#27ae60' : '#FF6B6B')
.layoutWeight(1)
}
.width('100%')
.padding(15)
.backgroundColor(this.messageType === 'success' ? '#F0FFF4' : '#FFF5F5')
.borderRadius(8)
.border({
width: 1,
color: this.messageType === 'success' ? '#90EE90' : '#FFE0E0'
})
}
@Builder
buildFileActionsCard() {
Column({ space: 15 }) {
Row({ space: 10 }) {
Text('✅')
.fontSize(24)
Text('生成成功')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#27ae60')
}
.alignItems(VerticalAlign.Center)
Text('Excel文件已成功生成并保存!您可以在保存的位置找到文件,支持在WPS Office、Microsoft Excel等应用中打开。')
.fontSize(14)
.fontColor('#666666')
.textAlign(TextAlign.Center)
Text('💡 提示:如需再次保存到其他位置,请重新点击生成按钮')
.fontSize(12)
.fontColor('#999999')
.textAlign(TextAlign.Center)
}
.width('100%')
.padding(20)
.backgroundColor('#F0FFF4')
.borderRadius(12)
.border({
width: 1,
color: '#90EE90'
})
}
// 生成Excel文件
async generateExcel() {
if (this.tableTitle.trim() === '') {
this.showMessage('请输入表格标题', 'error');
return;
}
if (!this.excelGenerator) {
this.showMessage('Excel生成器未初始化', 'error');
return;
}
try {
this.isGenerating = true;
this.message = '';
this.showFileActions = false;
// 构建表格数据
const tableData: ExcelTableData = {
title: this.tableTitle,
data: this.tableData,
// titleStyle: {
// fontWeight: 'bold',
// fontSize: 18,
// backgroundColor: '#4CAF50'
// },
// headerStyle: {
// fontWeight: 'bold',
// fontSize: 14,
// backgroundColor: '#E8F5E8'
// },
// dataStyle: {
// fontSize: 12
// }
}
// 生成Excel文件
const result = await this.excelGenerator?.generateExcel(tableData);
if (result?.success) {
this.showMessage(`Excel文件生成成功:${result.fileName}`, 'success');
console.log(`Excel文件已保存到:${result.filePath}`);
// 自动保存到本地
if (result.filePath && result.fileName) {
const saveResult = await this.excelGenerator?.saveToLocal(result.filePath, result.fileName);
if (saveResult?.success) {
this.showFileActions = true;
this.showMessage(saveResult.message, 'success');
} else {
this.showMessage(saveResult.message, 'error');
}
}
} else {
this.showMessage(result?.message, 'error');
}
} catch (error) {
console.error('生成Excel文件失败:', error);
this.showMessage(`生成失败:${error.message}`, 'error');
} finally {
this.isGenerating = false;
}
}
// 显示消息
showMessage(msg: string, type: 'success' | 'error') {
this.message = msg;
this.messageType = type;
// 3秒后自动清除消息
setTimeout(() => {
this.message = '';
this.messageType = '';
}, 3000);
}
}