在这里插入图片描述

文档转换功能允许用户将文档在不同格式之间进行转换,如PDF、Word、Excel和PPT。本文将详细讲解如何实现文档转换功能。

文档转换功能的设计思路

文档转换功能包括两个主要操作:格式转换文档合并。这两个功能都需要处理复杂的文档格式,在实际开发中我遇到了不少坑,特别是在处理不同编码格式的文档时。

构建文档转换的主界面

首先我们需要创建一个清晰的入口界面,让用户能够快速找到需要的功能。在转换页面中,我定义了文档转换的两个核心功能:

Widget _buildDocTab() {
  return ListView(
    padding: EdgeInsets.all(16.w),
    children: [
      ConvertItem(
        icon: Icons.description,
        title: '文档格式转换',
        subtitle: 'PDF/Word/Excel/PPT',
        onTap: () => _showConvertDetail('文档格式转换'),
      ),
      SizedBox(height: 12.h),
      ConvertItem(
        icon: Icons.merge,
        title: '文档合并',
        subtitle: '合并多个文档',
        onTap: () => _showConvertDetail('文档合并'),
      ),
    ],
  );
}

这里使用了 ListView 来组织界面元素,主要是考虑到后续可能会添加更多功能。ConvertItem 是我自定义的一个组件,封装了图标、标题、副标题和点击事件,这样做的好处是代码复用性高,而且修改样式时只需要改一个地方。

开发心得:最初我是直接用 ListTile 的,但后来发现它的样式定制不够灵活,特别是在适配 OpenHarmony 的时候,有些细节调整起来很麻烦,所以干脆自己封装了一个。

自定义 ConvertItem 组件

为了让界面更加统一和美观,我封装了一个 ConvertItem 组件:

class ConvertItem extends StatelessWidget {
  final IconData icon;
  final String title;
  final String subtitle;
  final VoidCallback onTap;

  const ConvertItem({
    Key? key,
    required this.icon,
    required this.title,
    required this.subtitle,
    required this.onTap,
  }) : super(key: key);

  
  Widget build(BuildContext context) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(12.r),
      ),
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(12.r),
        child: Padding(
          padding: EdgeInsets.all(16.w),
          child: Row(
            children: [
              Container(
                padding: EdgeInsets.all(12.w),
                decoration: BoxDecoration(
                  color: Theme.of(context).primaryColor.withOpacity(0.1),
                  borderRadius: BorderRadius.circular(8.r),
                ),
                child: Icon(icon, size: 28.sp, color: Theme.of(context).primaryColor),
              ),
              SizedBox(width: 16.w),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      title,
                      style: TextStyle(
                        fontSize: 16.sp,
                        fontWeight: FontWeight.w600,
                      ),
                    ),
                    SizedBox(height: 4.h),
                    Text(
                      subtitle,
                      style: TextStyle(
                        fontSize: 13.sp,
                        color: Colors.grey[600],
                      ),
                    ),
                  ],
                ),
              ),
              Icon(Icons.arrow_forward_ios, size: 16.sp, color: Colors.grey),
            ],
          ),
        ),
      ),
    );
  }
}

这个组件的设计参考了 Material Design 的卡片样式,但做了一些调整。特别注意:我在图标外面包了一层 Container,给它加了圆角背景色,这样视觉效果会更好。InkWell 提供了点击水波纹效果,提升了交互体验。

在实际使用中,我发现 elevation: 2 这个阴影值比较合适,太大了会显得突兀,太小了又不够明显。

文档格式的支持与选择器实现

应用支持多种文档格式的转换,包括PDF、Word、Excel和PPT。每种格式都有其特定的处理方式,在开发过程中需要考虑格式兼容性问题。

定义支持的文档格式

首先,我在代码中定义了一个枚举类来管理所有支持的文档格式:

enum DocumentFormat {
  pdf('PDF', 'application/pdf', ['.pdf']),
  word('Word', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', ['.docx', '.doc']),
  excel('Excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ['.xlsx', '.xls']),
  ppt('PPT', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', ['.pptx', '.ppt']);

  final String displayName;
  final String mimeType;
  final List<String> extensions;

  const DocumentFormat(this.displayName, this.mimeType, this.extensions);
}

使用枚举的好处是类型安全,而且可以把格式相关的信息都集中管理。mimeType 在文件选择和转换时会用到,extensions 用于文件过滤。这样做比直接用字符串要规范得多,后期维护也方便。

实现格式选择下拉框

接下来实现格式选择的下拉框,这里我做了一些优化:

class FormatSelector extends StatefulWidget {
  final DocumentFormat? initialFormat;
  final ValueChanged<DocumentFormat?> onChanged;

  const FormatSelector({
    Key? key,
    this.initialFormat,
    required this.onChanged,
  }) : super(key: key);

  
  State<FormatSelector> createState() => _FormatSelectorState();
}

class _FormatSelectorState extends State<FormatSelector> {
  DocumentFormat? _selectedFormat;

  
  void initState() {
    super.initState();
    _selectedFormat = widget.initialFormat;
  }

  
  Widget build(BuildContext context) {
    return DropdownButtonFormField<DocumentFormat>(
      value: _selectedFormat,
      hint: Text('选择目标格式', style: TextStyle(fontSize: 14.sp)),
      items: DocumentFormat.values.map((format) {
        return DropdownMenuItem(
          value: format,
          child: Row(
            children: [
              Icon(_getFormatIcon(format), size: 20.sp),
              SizedBox(width: 8.w),
              Text(format.displayName),
            ],
          ),
        );
      }).toList(),
      onChanged: (value) {
        setState(() {
          _selectedFormat = value;
        });
        widget.onChanged(value);
      },
      decoration: InputDecoration(
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(8.r),
        ),
        contentPadding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
      ),
    );
  }

  IconData _getFormatIcon(DocumentFormat format) {
    switch (format) {
      case DocumentFormat.pdf:
        return Icons.picture_as_pdf;
      case DocumentFormat.word:
        return Icons.description;
      case DocumentFormat.excel:
        return Icons.table_chart;
      case DocumentFormat.ppt:
        return Icons.slideshow;
    }
  }
}

这个选择器组件我加了图标显示,让用户能更直观地识别文档类型。注意:我把它封装成了 StatefulWidget,这样可以在组件内部管理选中状态,同时通过回调函数 onChanged 向外部传递选择结果。

在实际测试中,我发现给每个选项加上图标后,用户的操作错误率明显降低了,特别是对于不太熟悉英文缩写的用户来说。

文档合并功能的完整实现

文档合并功能允许用户选择多个文档,然后将它们合并成一个文件。这个功能在实际开发中比想象的要复杂,需要处理文件的顺序、格式的兼容性,还要考虑内存占用问题。

文件选择与管理

首先实现文件选择功能,这里我用了 file_picker 插件:

class DocumentMergeScreen extends StatefulWidget {
  const DocumentMergeScreen({Key? key}) : super(key: key);

  
  State<DocumentMergeScreen> createState() => _DocumentMergeScreenState();
}

class _DocumentMergeScreenState extends State<DocumentMergeScreen> {
  final List<PlatformFile> _selectedFiles = [];
  DocumentFormat? _targetFormat;
  bool _isProcessing = false;

  Future<void> _pickFiles() async {
    try {
      FilePickerResult? result = await FilePicker.platform.pickFiles(
        allowMultiple: true,
        type: FileType.custom,
        allowedExtensions: ['pdf', 'docx', 'doc', 'xlsx', 'xls', 'pptx', 'ppt'],
      );

      if (result != null) {
        setState(() {
          _selectedFiles.addAll(result.files);
        });
      }
    } catch (e) {
      _showErrorDialog('文件选择失败: ${e.toString()}');
    }
  }
}

这里有几个关键点:首先是 allowMultiple: true 允许多选,这是合并功能的基础。然后通过 allowedExtensions 限制了可选的文件类型,避免用户选择不支持的格式。

踩坑记录:最初我没有加 try-catch,结果在某些设备上文件选择器崩溃时整个应用都挂了。后来加上异常处理后,即使选择器出问题也能优雅地提示用户。

文件列表展示与排序

选择文件后,需要展示文件列表并支持拖拽排序:

Widget _buildFileList() {
  if (_selectedFiles.isEmpty) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(Icons.folder_open, size: 64.sp, color: Colors.grey),
          SizedBox(height: 16.h),
          Text(
            '还没有选择文件',
            style: TextStyle(fontSize: 16.sp, color: Colors.grey),
          ),
          SizedBox(height: 8.h),
          Text(
            '点击下方按钮添加文件',
            style: TextStyle(fontSize: 14.sp, color: Colors.grey[400]),
          ),
        ],
      ),
    );
  }

  return ReorderableListView.builder(
    itemCount: _selectedFiles.length,
    onReorder: (oldIndex, newIndex) {
      setState(() {
        if (newIndex > oldIndex) {
          newIndex -= 1;
        }
        final file = _selectedFiles.removeAt(oldIndex);
        _selectedFiles.insert(newIndex, file);
      });
    },
    itemBuilder: (context, index) {
      final file = _selectedFiles[index];
      return _buildFileItem(file, index);
    },
  );
}

ReorderableListView 是 Flutter 提供的可拖拽排序列表,非常适合这个场景。在 onReorder 回调中处理排序逻辑时,要注意 newIndex 的调整,这是一个常见的坑点。

实用技巧:当列表为空时,我显示了一个友好的空状态提示,这比直接显示空白页面要好得多,能引导用户进行下一步操作。

单个文件项的展示

每个文件项需要显示文件信息和操作按钮:

Widget _buildFileItem(PlatformFile file, int index) {
  final fileSize = _formatFileSize(file.size);
  final fileExtension = file.extension?.toUpperCase() ?? 'UNKNOWN';

  return Card(
    key: ValueKey(file.path),
    margin: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
    child: ListTile(
      leading: CircleAvatar(
        backgroundColor: _getFormatColor(fileExtension),
        child: Text(
          fileExtension,
          style: TextStyle(fontSize: 10.sp, fontWeight: FontWeight.bold),
        ),
      ),
      title: Text(
        file.name,
        maxLines: 1,
        overflow: TextOverflow.ellipsis,
      ),
      subtitle: Text('大小: $fileSize'),
      trailing: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text(
            '${index + 1}',
            style: TextStyle(
              fontSize: 16.sp,
              fontWeight: FontWeight.bold,
              color: Colors.grey,
            ),
          ),
          SizedBox(width: 8.w),
          IconButton(
            icon: const Icon(Icons.close),
            onPressed: () {
              setState(() {
                _selectedFiles.removeAt(index);
              });
            },
          ),
        ],
      ),
    ),
  );
}

String _formatFileSize(int bytes) {
  if (bytes < 1024) return '$bytes B';
  if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
  return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
}

Color _getFormatColor(String extension) {
  switch (extension.toLowerCase()) {
    case 'pdf':
      return Colors.red;
    case 'docx':
    case 'doc':
      return Colors.blue;
    case 'xlsx':
    case 'xls':
      return Colors.green;
    case 'pptx':
    case 'ppt':
      return Colors.orange;
    default:
      return Colors.grey;
  }
}

这里我给每种文件格式配了不同的颜色,让界面更有辨识度。_formatFileSize 函数用来格式化文件大小,这样显示出来更友好。注意key: ValueKey(file.path) 很重要,它能确保在拖拽排序时 Flutter 能正确识别每个列表项。

转换详情对话框

当用户点击转换按钮时,显示详细的转换配置对话框:

void _showConvertDetail(String title) {
  showDialog(
    context: context,
    barrierDismissible: false,
    builder: (context) => AlertDialog(
      title: Row(
        children: [
          Icon(Icons.settings, size: 24.sp),
          SizedBox(width: 8.w),
          Text(title),
        ],
      ),
      content: SingleChildScrollView(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              '已选择 ${_selectedFiles.length} 个文件',
              style: TextStyle(
                fontSize: 14.sp,
                fontWeight: FontWeight.w500,
              ),
            ),
            SizedBox(height: 16.h),
            Text(
              '目标格式',
              style: TextStyle(
                fontSize: 13.sp,
                color: Colors.grey[600],
              ),
            ),
            SizedBox(height: 8.h),
            FormatSelector(
              initialFormat: _targetFormat,
              onChanged: (format) {
                _targetFormat = format;
              },
            ),
            SizedBox(height: 16.h),
            Text(
              '输出文件名',
              style: TextStyle(
                fontSize: 13.sp,
                color: Colors.grey[600],
              ),
            ),
            SizedBox(height: 8.h),
            TextField(
              decoration: InputDecoration(
                hintText: '合并后的文件名',
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(8.r),
                ),
                contentPadding: EdgeInsets.symmetric(
                  horizontal: 12.w,
                  vertical: 8.h,
                ),
              ),
              onChanged: (value) {
                // 保存文件名
              },
            ),
          ],
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('取消'),
        ),
        ElevatedButton(
          onPressed: () {
            if (_targetFormat == null) {
              _showErrorDialog('请选择目标格式');
              return;
            }
            Navigator.pop(context);
            _startConversion();
          },
          child: const Text('开始转换'),
        ),
      ],
    ),
  );
}

这个对话框的设计考虑了用户体验:首先显示已选文件数量,让用户确认;然后是格式选择和文件名输入。我把 barrierDismissible 设为 false,防止用户误触关闭对话框。

设计细节:在标题旁边加了一个设置图标,让对话框看起来更专业。按钮也做了区分,取消用 TextButton,确认用 ElevatedButton,视觉层次更清晰。

文件处理的复杂性

文档转换比图片转换更加复杂,因为不同的文档格式有不同的结构和特性。在实际应用中,我们可能需要使用专门的库来处理不同的文档格式。

转换进度的显示

对于大型文档的转换,我们可能需要显示转换进度。这可以通过使用ProgressIndicator来实现。

showDialog(
  context: context,
  builder: (context) => AlertDialog(
    title: const Text('转换中...'),
    content: const LinearProgressIndicator(),
  ),
);

文档转换的核心逻辑实现

文档转换比图片转换要复杂得多,因为不同的文档格式有完全不同的内部结构。在实际项目中,我使用了 pdfsyncfusion_flutter_pdf 等库来处理转换。

转换进度管理

对于大型文档的转换,必须要有进度显示,否则用户会以为程序卡死了:

Future<void> _startConversion() async {
  setState(() {
    _isProcessing = true;
  });

  try {
    await _showProgressDialog();
    
    final result = await _performConversion();
    
    Navigator.pop(context); // 关闭进度对话框
    
    if (result.success) {
      _showSuccessDialog(result.outputPath);
    } else {
      _showErrorDialog(result.errorMessage);
    }
  } catch (e) {
    Navigator.pop(context);
    _showErrorDialog('转换失败: ${e.toString()}');
  } finally {
    setState(() {
      _isProcessing = false;
    });
  }
}

Future<void> _showProgressDialog() async {
  showDialog(
    context: context,
    barrierDismissible: false,
    builder: (context) => WillPopScope(
      onWillPop: () async => false,
      child: AlertDialog(
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            CircularProgressIndicator(),
            SizedBox(height: 16.h),
            Text(
              '正在转换文档...',
              style: TextStyle(fontSize: 16.sp),
            ),
            SizedBox(height: 8.h),
            Text(
              '请稍候,这可能需要几分钟',
              style: TextStyle(fontSize: 13.sp, color: Colors.grey[600]),
            ),
          ],
        ),
      ),
    ),
  );
}

这里用了 WillPopScope 来禁止用户在转换过程中返回,避免中断转换导致文件损坏。barrierDismissible: false 也是同样的目的。

经验分享:最初我只显示了一个转圈的进度条,但用户反馈说不知道要等多久。后来加上了"这可能需要几分钟"的提示,用户的焦虑感明显降低了。

实际转换逻辑

这里是文档合并的核心代码,以 PDF 合并为例:

class ConversionResult {
  final bool success;
  final String? outputPath;
  final String? errorMessage;

  ConversionResult({
    required this.success,
    this.outputPath,
    this.errorMessage,
  });
}

Future<ConversionResult> _performConversion() async {
  try {
    if (_targetFormat == DocumentFormat.pdf) {
      return await _mergeToPdf();
    } else {
      return ConversionResult(
        success: false,
        errorMessage: '暂不支持转换为 ${_targetFormat?.displayName} 格式',
      );
    }
  } catch (e) {
    return ConversionResult(
      success: false,
      errorMessage: e.toString(),
    );
  }
}

Future<ConversionResult> _mergeToPdf() async {
  final PdfDocument outputDocument = PdfDocument();

  try {
    for (var file in _selectedFiles) {
      if (file.path == null) continue;

      final bytes = await File(file.path!).readAsBytes();
      
      if (file.extension?.toLowerCase() == 'pdf') {
        // 合并 PDF 文件
        final PdfDocument inputDocument = PdfDocument(inputBytes: bytes);
        outputDocument.pages.addAll(inputDocument.pages);
        inputDocument.dispose();
      } else {
        // 其他格式需要先转换为 PDF
        final convertedBytes = await _convertToPdf(file);
        if (convertedBytes != null) {
          final PdfDocument inputDocument = PdfDocument(inputBytes: convertedBytes);
          outputDocument.pages.addAll(inputDocument.pages);
          inputDocument.dispose();
        }
      }
    }

    // 保存合并后的文档
    final outputPath = await _saveDocument(outputDocument);
    outputDocument.dispose();

    return ConversionResult(
      success: true,
      outputPath: outputPath,
    );
  } catch (e) {
    outputDocument.dispose();
    return ConversionResult(
      success: false,
      errorMessage: '合并失败: ${e.toString()}',
    );
  }
}

这段代码的关键在于循环处理每个文件,如果是 PDF 就直接合并,如果是其他格式就先转换。重要提示:每次使用完 PdfDocument 后一定要调用 dispose(),否则会造成内存泄漏,特别是处理大文件时。

我在开发过程中就遇到过这个问题,合并几个大文件后应用就崩溃了,后来加上内存管理才解决。

文件保存与路径处理

转换完成后需要保存文件,这里要处理好路径和权限问题:

Future<String> _saveDocument(PdfDocument document) async {
  final bytes = await document.save();
  
  // 获取应用文档目录
  final directory = await getApplicationDocumentsDirectory();
  final timestamp = DateTime.now().millisecondsSinceEpoch;
  final fileName = 'merged_document_$timestamp.pdf';
  final filePath = '${directory.path}/$fileName';
  
  final file = File(filePath);
  await file.writeAsBytes(bytes);
  
  return filePath;
}

void _showSuccessDialog(String? filePath) {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: Row(
        children: [
          Icon(Icons.check_circle, color: Colors.green, size: 28.sp),
          SizedBox(width: 8.w),
          const Text('转换成功'),
        ],
      ),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('文档已成功转换并保存'),
          if (filePath != null) ...[
            SizedBox(height: 12.h),
            Text(
              '保存位置:',
              style: TextStyle(
                fontSize: 13.sp,
                fontWeight: FontWeight.w500,
              ),
            ),
            SizedBox(height: 4.h),
            Container(
              padding: EdgeInsets.all(8.w),
              decoration: BoxDecoration(
                color: Colors.grey[100],
                borderRadius: BorderRadius.circular(4.r),
              ),
              child: Text(
                filePath,
                style: TextStyle(fontSize: 12.sp),
              ),
            ),
          ],
        ],
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('确定'),
        ),
        if (filePath != null)
          ElevatedButton.icon(
            onPressed: () {
              Navigator.pop(context);
              _openFile(filePath);
            },
            icon: const Icon(Icons.open_in_new, size: 18),
            label: const Text('打开文件'),
          ),
      ],
    ),
  );
}

成功对话框里我加了文件路径显示和"打开文件"按钮,这样用户可以直接查看转换结果。文件名用时间戳命名,避免重复。

实用建议:在 Android 上,如果要保存到公共目录(如 Downloads),需要申请存储权限。我这里保存到应用私有目录,不需要额外权限,更简单。

错误处理与用户反馈

在文档转换过程中,可能会出现各种错误:文件损坏、格式不支持、内存不足等。完善的错误处理能大大提升用户体验。

统一的错误提示

我封装了一个通用的错误提示方法:

void _showErrorDialog(String message) {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: Row(
        children: [
          Icon(Icons.error_outline, color: Colors.red, size: 28.sp),
          SizedBox(width: 8.w),
          const Text('操作失败'),
        ],
      ),
      content: Text(message),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('知道了'),
        ),
      ],
    ),
  );
}

简单但有效,红色的错误图标能立即引起用户注意。

文件格式验证

在转换前验证文件格式,避免浪费时间:

bool _validateFiles() {
  if (_selectedFiles.isEmpty) {
    _showErrorDialog('请先选择要转换的文件');
    return false;
  }

  // 检查文件大小
  final totalSize = _selectedFiles.fold<int>(
    0,
    (sum, file) => sum + file.size,
  );
  
  if (totalSize > 100 * 1024 * 1024) { // 100MB
    _showErrorDialog('文件总大小超过限制(最大100MB)');
    return false;
  }

  // 检查文件格式一致性(合并时需要)
  final extensions = _selectedFiles
      .map((f) => f.extension?.toLowerCase())
      .toSet();
  
  if (extensions.length > 1 && _targetFormat == null) {
    _showErrorDialog('选择了不同格式的文件,请指定目标格式');
    return false;
  }

  return true;
}

这些验证能在转换前就发现问题,避免用户等了半天才发现转换失败。特别注意:文件大小限制要根据实际情况调整,我这里设的 100MB 是考虑到移动设备的内存限制。

性能优化与内存管理

处理大文档时,性能和内存管理至关重要。

分批处理大文件

Future<ConversionResult> _mergeToPdfOptimized() async {
  const int batchSize = 5; // 每批处理5个文件
  final PdfDocument outputDocument = PdfDocument();

  try {
    for (int i = 0; i < _selectedFiles.length; i += batchSize) {
      final end = (i + batchSize < _selectedFiles.length)
          ? i + batchSize
          : _selectedFiles.length;
      
      final batch = _selectedFiles.sublist(i, end);
      
      for (var file in batch) {
        // 处理单个文件
        await _processSingleFile(file, outputDocument);
      }
      
      // 每批处理完后稍作延迟,让系统有时间回收内存
      await Future.delayed(const Duration(milliseconds: 100));
    }

    final outputPath = await _saveDocument(outputDocument);
    outputDocument.dispose();

    return ConversionResult(success: true, outputPath: outputPath);
  } catch (e) {
    outputDocument.dispose();
    return ConversionResult(success: false, errorMessage: e.toString());
  }
}

分批处理能有效控制内存占用,特别是在处理几十个文件时。这个优化让应用在低端设备上也能稳定运行。

总结

通过这个文档转换功能的实现,我们学到了几个关键点:

首先是用户体验设计,从文件选择、排序到进度显示,每个环节都要考虑用户的感受。其次是错误处理,要预见各种可能出错的情况并给出友好的提示。最后是性能优化,处理大文件时必须注意内存管理,否则应用很容易崩溃。

在实际开发中,我遇到了不少坑,比如内存泄漏、文件权限、格式兼容性等问题。但通过不断测试和优化,最终实现了一个稳定可用的文档转换工具。

希望这篇文章能帮助你在开发类似功能时少走弯路。代码都是从实际项目中提取的,可以直接参考使用。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐