1、权限

 {
      "name": "ohos.permission.READ_WRITE_DOWNLOAD_DIRECTORY",
      "reason": "$string:permission_reason",
      "usedScene": {
        "abilities": ["EntryAbility"],
        "when": "inuse"
      }
    }

2、工具

// utils/CSVExporter.ts

import fs from '@ohos.file.fs';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * 轨迹点接口
 */
export interface ITrackPoint {
  latitude: number; // 纬度
  longitude: number; // 经度
  altitude: number; // 海拔(米)
  speed: number; // 速度(m/s)
  timestamp: number; // 时间戳(毫秒)
}

/**
 * CSV 导出配置
 */
export interface CSVExportOptions {
  fileName?: string; // 文件名(不含扩展名)
  includeHeader?: boolean; // 是否包含表头(默认 true)
  delimiter?: string; // 分隔符(默认逗号)
}

/**
 * CSV 导出结果
 */
export interface CSVExportResult {
  success: boolean;
  filePath?: string;
  error?: string;
  fileSize?: number;
}

/**
 * CSV 导出器
 */
export class CSVExporter {
  /**
   * 将轨迹点数组转换为 CSV 格式字符串
   */
  /**
   * 将轨迹点数组转换为 CSV 格式字符串
   */
  static convertToCSV(
    trackPoints: ITrackPoint[],
    options?: CSVExportOptions
  ): string {

    if (!trackPoints || trackPoints.length === 0) {
      return '';
    }

    const delimiter = options?.delimiter || ',';
    const includeHeader = options?.includeHeader !== false;

    let csv = '';

    if (includeHeader) {
      const headers = ['latitude', 'longitude', 'altitude', 'speed', 'timestamp'];
      csv = headers.join(delimiter) + '\n';
    }

    trackPoints.forEach((point: ITrackPoint) => {
      const row: Array<number | string> = [
        point.latitude,
        point.longitude,
        point.altitude,
        point.speed,
        point.timestamp
      ];

      csv += row.join(delimiter) + '\n';
    });

    return csv;
  }

  /**
   * 保存 CSV 文件到本地
   */
  static async saveCSVFile(
    context: Context,
    csvContent: string,
    fileName: string = 'track'
  ): Promise<string | null> {

    if (!csvContent) {
      return null;
    }

    try {
      // 获取文件保存路径
      const filesDir = context.filesDir;
      const timestamp = Date.now();
      const filePath = `${filesDir}/${fileName}_${timestamp}.csv`;

      // 创建并写入文件
      const file = fs.openSync(filePath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY);
      fs.writeSync(file.fd, csvContent);
      fs.closeSync(file);


      return filePath;

    } catch (err) {
      const error = err as BusinessError;
      return null;
    }
  }

  /**
   * 一键导出:转换 + 保存
   */
  static async exportToCSV(
    context: Context,
    trackPoints: ITrackPoint[],
    options?: CSVExportOptions
  ): Promise<CSVExportResult> {

    try {

      // 验证数据
      if (!trackPoints || trackPoints.length === 0) {
        return {
          success: false,
          error: '轨迹点数组为空'
        };
      }

      // 1. 转换为 CSV 格式
      const csvContent = CSVExporter.convertToCSV(trackPoints, options);

      if (!csvContent) {
        return {
          success: false,
          error: 'CSV 内容为空'
        };
      }

      // 2. 保存到文件
      const fileName = options?.fileName || 'track';
      const filePath = await CSVExporter.saveCSVFile(context, csvContent, fileName);

      if (filePath) {
        return {
          success: true,
          filePath: filePath,
          fileSize: csvContent.length
        };
      } else {
        return {
          success: false,
          error: '文件保存失败'
        };
      }

    } catch (err) {
      const error = err as BusinessError;
      return {
        success: false,
        error: error.message || '未知错误'
      };
    }
  }

  /**
   * 验证轨迹点数据
   */
  static validateTrackPoint(point: ESObject): boolean {
    if (typeof point !== 'object' || point === null) {
      return false;
    }

    return (
      typeof point.latitude === 'number' &&
        typeof point.longitude === 'number' &&
        typeof point.altitude === 'number' &&
        typeof point.speed === 'number' &&
        typeof point.timestamp === 'number'
    );
  }

  /**
   * 验证轨迹点数组
   */
  static validateTrackPoints(trackPoints: ESObject[]): boolean {
    if (!Array.isArray(trackPoints) || trackPoints.length === 0) {
      return false;
    }

    return trackPoints.every((point: ESObject) => CSVExporter.validateTrackPoint(point));
  }

  /**
   * 转换 ESObject 为 ITrackPoint
   */
  static convertToTrackPoint(obj: ESObject): ITrackPoint | null {
    if (!CSVExporter.validateTrackPoint(obj)) {
      return null;
    }

    return {
      latitude: obj.latitude as number,
      longitude: obj.longitude as number,
      altitude: obj.altitude as number,
      speed: obj.speed as number,
      timestamp: obj.timestamp as number
    };
  }

  /**
   * 转换 ESObject 数组为 ITrackPoint 数组
   */

  static convertToTrackPoints(arr: ESObject[]): ITrackPoint[] {
    const result: ITrackPoint[] = [];

    arr.forEach((obj: ESObject) => {
      const trackPoint = CSVExporter.convertToTrackPoint(obj);
      if (trackPoint !== null) {
        result.push(trackPoint);
      }
    });

    return result;
  }
}

3.使用

import { CSVExporter, CSVExportOptions, CSVExportResult, ITrackPoint } from './CSVExporter';
import fs from '@ohos.file.fs';
import { BusinessError } from '@kit.BasicServicesKit';
import picker from '@ohos.file.picker';

@Entry
@Component
struct TestCSVExportPage {
  @State filePath: string = '';
  @State csvContent: string = '';
  @State showPreview: boolean = false;
  private context = getContext(this);
  // 测试数据
  private testData: ITrackPoint[] = [
    {
      latitude: 36.089412,
      longitude: 120.377728,
      altitude: 4.9,
      speed: 0.0,
      timestamp: 1717223162000
    },
    {
      latitude: 36.090038,
      longitude: 120.377650,
      altitude: 8.2,
      speed: 3.5,
      timestamp: 1717223295000
    },
    {
      latitude: 36.091056,
      longitude: 120.377515,
      altitude: 12.5,
      speed: 5.2,
      timestamp: 1717223430000
    },
    {
      latitude: 36.091212,
      longitude: 120.376953,
      altitude: 15.3,
      speed: 6.8,
      timestamp: 1717223565000
    },
    {
      latitude: 36.091181,
      longitude: 120.376372,
      altitude: 18.7,
      speed: 4.1,
      timestamp: 1717223700000
    },
    {
      latitude: 36.091181,
      longitude: 120.375636,
      altitude: 22.1,
      speed: 7.5,
      timestamp: 1717223835000
    },
    {
      latitude: 36.091165,
      longitude: 120.374571,
      altitude: 25.8,
      speed: 5.8,
      timestamp: 1717223970000
    },
    {
      latitude: 36.091150,
      longitude: 120.373912,
      altitude: 28.4,
      speed: 3.2,
      timestamp: 1717224105000
    },
    {
      latitude: 36.091197,
      longitude: 120.373292,
      altitude: 31.2,
      speed: 8.2,
      timestamp: 1717224240000
    },
    {
      latitude: 36.091744,
      longitude: 120.373195,
      altitude: 35.6,
      speed: 4.5,
      timestamp: 1717224375000
    },
    {
      latitude: 36.092496,
      longitude: 120.373195,
      altitude: 38.9,
      speed: 6.1,
      timestamp: 1717224510000
    },
    {
      latitude: 36.092731,
      longitude: 120.373176,
      altitude: 42.3,
      speed: 2.8,
      timestamp: 1717224645000
    },
    {
      latitude: 36.093310,
      longitude: 120.373157,
      altitude: 45.7,
      speed: 0.0,
      timestamp: 1717224780000
    }
  ];

  /**
   * 导出 CSV
   */
  async exportCSV() {
    try {

      const options: CSVExportOptions = {
        fileName: '我的轨迹',
        includeHeader: false,
        delimiter: ','
      };

      const result: CSVExportResult = await CSVExporter.exportToCSV(
        this.context,
        this.testData,
        options
      );

      if (result.success && result.filePath) {
        this.filePath = result.filePath;

        AlertDialog.show({
          title: '导出成功',
          message: `已成功导出 ${this.testData.length} 个轨迹点\n\n文件大小: ${result.fileSize} 字节\n\n路径:\n${result.filePath}`,
          confirm: {
            value: '确定',
            action: () => {
            }
          }
        });
      } else {
      }

    } catch (err) {
      console.error('导出失败:', err);
    }
  }

  /**
   * 读取并预览 CSV 文件内容
   */
  async previewCSV() {
    if (!this.filePath) {
      AlertDialog.show({
        title: '提示',
        message: '请先导出 CSV 文件',
        confirm: {
          value: '确定', action: () => {
          }
        }
      });
      return;
    }

    try {
      // 读取文件内容
      const file = fs.openSync(this.filePath, fs.OpenMode.READ_ONLY);
      const stat = fs.statSync(this.filePath);
      const buffer = new ArrayBuffer(stat.size);
      fs.readSync(file.fd, buffer);
      fs.closeSync(file);

      // 转换为字符串
      const uint8Array = new Uint8Array(buffer);
      this.csvContent = this.arrayBufferToString(uint8Array);

      // 显示预览
      this.showPreview = true;

      console.log('CSV 内容:', this.csvContent);

    } catch (err) {
      const error = err as BusinessError;
      console.error('读取文件失败:', error);
      AlertDialog.show({
        title: '读取失败',
        message: `无法读取文件: ${error.message}`,
        confirm: {
          value: '确定', action: () => {
          }
        }
      });
    }
  }

  /**
   * ArrayBuffer 转字符串
   */
  arrayBufferToString(buffer: Uint8Array): string {
    let str = '';
    for (let i = 0; i < buffer.length; i++) {
      str += String.fromCharCode(buffer[i]);
    }
    return str;
  }

  async shareCSV() {
    if (!this.filePath) {
      AlertDialog.show({
        title: '提示',
        message: '请先导出 CSV 文件',
        confirm: {
          value: '确定', action: () => {
          }
        }
      });
      return;
    }

    try {

      // 1. 配置文件选择器
      const documentSaveOptions = new picker.DocumentSaveOptions();
      documentSaveOptions.newFileNames = ['轨迹数据.csv'];
      documentSaveOptions.fileSuffixChoices = ['.csv'];

      // 2. 打开保存对话框
      const documentPicker = new picker.DocumentViewPicker();
      const uris: string[] = await documentPicker.save(documentSaveOptions);


      if (!uris || uris.length === 0) {
        return;
      }

      const targetUri = uris[0];

      // 读取源文件
      const sourceFile = fs.openSync(this.filePath, fs.OpenMode.READ_ONLY);
      const stat = fs.statSync(this.filePath);
      const buffer = new ArrayBuffer(stat.size);
      fs.readSync(sourceFile.fd, buffer);
      fs.closeSync(sourceFile);


      // 写入目标文件(URI)
      const targetFile = fs.openSync(targetUri, fs.OpenMode.WRITE_ONLY | fs.OpenMode.CREATE);
      fs.writeSync(targetFile.fd, buffer);
      fs.closeSync(targetFile);


      AlertDialog.show({
        title: '保存成功',
        message: '文件已保存到外部存储\n\n可以用文件管理器或其他应用打开查看',
        confirm: {
          value: '确定', action: () => {
          }
        }
      });

    } catch (err) {
      const error = err as BusinessError;
      console.error('错误代码:', error.code);
      console.error('错误信息:', error.message);

      AlertDialog.show({
        title: '分享失败',
        message: `无法保存文件\n\n错误代码: ${error.code}\n错误信息: ${error.message}`,
        confirm: {
          value: '确定', action: () => {
          }
        }
      });
    }
  }

  /**
   * 关闭预览
   */
  closePreview() {
    this.showPreview = false;
  }

  build() {
    Stack() {
      // 主界面
      Column() {
        Text('CSV 导出测试')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .margin({ top: 50, bottom: 30 })

        Text(`测试数据: ${this.testData.length} 个轨迹点`)
          .fontSize(16)
          .margin({ bottom: 30 })

        // 按钮组
        Column({ space: 15 }) {
          Button('点击导出 CSV')
            .width('80%')
            .height(50)
            .fontSize(18)
            .backgroundColor('#0FD7B8')
            .onClick(() => {
              this.exportCSV();
            })

          if (this.filePath) {
            Button(' 预览 CSV 内容')
              .width('80%')
              .height(50)
              .fontSize(18)
              .backgroundColor('#4A90E2')
              .onClick(() => {
                this.previewCSV();
              })

            Button(' 保存到手机')
              .width('80%')
              .height(50)
              .fontSize(18)
              .backgroundColor('#FF9800')
              .onClick(() => {
                this.shareCSV();
              })
          }
        }
        .margin({ bottom: 20 })


        if (this.filePath) {
          Column() {
            Text(' 文件路径')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .margin({ bottom: 8 })

            Text(this.filePath)
              .fontSize(11)
              .fontColor('#666')
              .textAlign(TextAlign.Center)
              .maxLines(4)
          }
          .width('90%')
          .padding(15)
          .backgroundColor('#e8f5e9')
          .borderRadius(8)
        }
      }
      .width('100%')
      .height('100%')

      // 预览弹窗
      if (this.showPreview) {
        Column() {
          // 标题栏
          Row() {
            Text('CSV 内容预览')
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
              .layoutWeight(1)

            Button('关闭')
              .fontSize(14)
              .backgroundColor('#FF5252')
              .onClick(() => {
                this.closePreview();
              })
          }
          .width('100%')
          .padding(15)
          .backgroundColor('#FFFFFF')
          .borderRadius({ topLeft: 12, topRight: 12 })

          // 内容区域
          Scroll() {
            Text(this.csvContent)
              .fontSize(12)
              .fontColor('#333')
              .fontFamily('monospace')
              .width('100%')
              .padding(15)
          }
          .width('100%')
          .layoutWeight(1)
          .backgroundColor('#F5F5F5')
          .scrollBar(BarState.Auto)
        }
        .width('90%')
        .height('70%')
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .shadow({
          radius: 20,
          color: '#000000',
          offsetX: 0,
          offsetY: 5
        })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.showPreview ? 'rgba(0, 0, 0, 0.5)' : '#FFFFFF')
  }
}

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐