如何导出Excel文件 -- excel_hm介绍 ##三方SDK##

前阵子在和朋友讨论,记账类的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);
   }
}
相关推荐
不爱吃糖的程序媛27 分钟前
2026年Electron 鸿蒙PC环境搭建指南
人工智能·华为·harmonyos
nashane31 分钟前
HarmonyOS 6学习:长截图功能开发中的滚动拼接与权限处理实战
人工智能·华为·harmonyos
大师兄66682 小时前
从零开发一个 HarmonyOS 输入法——KikaInputMethod 完整拆解
harmonyos·服务卡片·harmonyos6·formkit
Python私教7 小时前
鸿蒙 NEXT 也能接 MCP?用 ArkTS 跑通 AI Agent 工具链
人工智能·华为·harmonyos
Swift社区10 小时前
分布式能力在鸿蒙 PC 上到底怎么用?
分布式·华为·harmonyos
nashane19 小时前
HarmonyOS 6学习:外接键盘CapsLock与长截图功能的实战调试与完整解决方案
学习·华为·计算机外设·harmonyos
aqi001 天前
一文理清 HarmonyOS 6.0.2 涵盖的十个升级点
android·华为·harmonyos·鸿蒙·harmony
环信即时通讯云1 天前
环信Flutter UIKit适配鸿蒙实战指南
flutter·华为·harmonyos
Swift社区1 天前
鸿蒙 PC 应用启动优化全解析
华为·harmonyos