如何导出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);
   }
}
相关推荐
程序员小刘5 小时前
【HarmonyOS 5】鸿蒙Taro跨端框架
华为·harmonyos·taro
SuperHeroWu77 小时前
【 HarmonyOS 5 入门系列 】鸿蒙HarmonyOS示例项目讲解
华为·harmonyos·arkts·讲解·arkui·空ability示例项目
二流小码农9 小时前
鸿蒙开发:hvigorw,编译构建,实现命令打包
android·ios·harmonyos
simple丶11 小时前
【HarmonyOS】项目工程化工具 持续更迭...
harmonyos·arkts·arkui
lqj_本人11 小时前
鸿蒙OS&UniApp微服务架构实践:从设计到鸿蒙部署#三方框架 #Uniapp
架构·uni-app·harmonyos
上海张律师12 小时前
如何监听组件再次显示的事件?
harmonyos
__Benco12 小时前
OpenHarmony平台驱动使用(四),GPIO
人工智能·驱动开发·harmonyos
王二蛋与他的张大花12 小时前
HarmonyOS运动开发:精准估算室内运动的距离、速度与步幅
harmonyos
用户85610176149513 小时前
Harmony 0S5 代码高亮、跳转(跨语言支持ArkTS与C++)、格式化与折叠技巧
harmonyos
郑知鱼13 小时前
【拥抱鸿蒙】HarmonyOS NEXT实现双路预览并识别文字
华为·ocr·harmonyos·鸿蒙·移动端·鸿蒙next·ohos