找回密码
 立即注册
首页 业界区 业界 Flutter CSV导入导出:大数据处理与用户体验优化 ...

Flutter CSV导入导出:大数据处理与用户体验优化

廖雯华 2025-9-18 05:05:24
Flutter CSV导入导出:大数据处理与用户体验优化

本文基于BeeCount(蜜蜂记账)项目的实际开发经验,深入探讨如何在Flutter应用中实现高效、用户友好的CSV数据导入导出功能。
项目背景

BeeCount(蜜蜂记账)是一款开源、简洁、无广告的个人记账应用。所有财务数据完全由用户掌控,支持本地存储和可选的云端同步,确保数据绝对安全。
引言

数据的导入导出是现代应用的基本需求,特别是对于财务管理类应用。用户希望能够:

  • 从其他记账软件迁移数据
  • 定期备份数据到本地文件
  • 在电脑上进行数据分析处理
  • 与会计软件进行数据交换
CSV格式因其简单性和通用性,成为了数据交换的首选格式。但在移动端实现高效的CSV处理并不简单,需要考虑性能、内存占用、用户体验等多个方面。
CSV处理架构设计

核心组件架构
  1. // CSV处理服务接口
  2. abstract class CsvService {
  3.   Future<CsvExportResult> exportTransactions({
  4.     required int ledgerId,
  5.     required DateTimeRange dateRange,
  6.     required CsvExportOptions options,
  7.   });
  8.   Future<CsvImportResult> importTransactions({
  9.     required String csvContent,
  10.     required int ledgerId,
  11.     required CsvImportOptions options,
  12.     void Function(double progress)? onProgress,
  13.   });
  14.   Future<List<CsvColumn>> analyzeCsvStructure(String csvContent);
  15. }
  16. // CSV导出选项
  17. class CsvExportOptions {
  18.   final bool includeHeader;
  19.   final String separator;
  20.   final String encoding;
  21.   final List<String> columns;
  22.   final TransactionFilter? filter;
  23.   const CsvExportOptions({
  24.     this.includeHeader = true,
  25.     this.separator = ',',
  26.     this.encoding = 'utf-8',
  27.     required this.columns,
  28.     this.filter,
  29.   });
  30. }
  31. // CSV导入选项
  32. class CsvImportOptions {
  33.   final bool hasHeader;
  34.   final String separator;
  35.   final String encoding;
  36.   final Map<String, String> columnMapping;
  37.   final bool skipDuplicates;
  38.   final ConflictResolution conflictResolution;
  39.   const CsvImportOptions({
  40.     this.hasHeader = true,
  41.     this.separator = ',',
  42.     this.encoding = 'utf-8',
  43.     required this.columnMapping,
  44.     this.skipDuplicates = true,
  45.     this.conflictResolution = ConflictResolution.skip,
  46.   });
  47. }
  48. // 导出结果
  49. class CsvExportResult {
  50.   final bool success;
  51.   final String? filePath;
  52.   final int recordCount;
  53.   final String? error;
  54.   const CsvExportResult({
  55.     required this.success,
  56.     this.filePath,
  57.     required this.recordCount,
  58.     this.error,
  59.   });
  60.   factory CsvExportResult.success({
  61.     required String filePath,
  62.     required int recordCount,
  63.   }) {
  64.     return CsvExportResult(
  65.       success: true,
  66.       filePath: filePath,
  67.       recordCount: recordCount,
  68.     );
  69.   }
  70.   factory CsvExportResult.failure(String error) {
  71.     return CsvExportResult(
  72.       success: false,
  73.       error: error,
  74.       recordCount: 0,
  75.     );
  76.   }
  77. }
  78. // 导入结果
  79. class CsvImportResult {
  80.   final bool success;
  81.   final int totalRows;
  82.   final int importedRows;
  83.   final int skippedRows;
  84.   final List<ImportError> errors;
  85.   const CsvImportResult({
  86.     required this.success,
  87.     required this.totalRows,
  88.     required this.importedRows,
  89.     required this.skippedRows,
  90.     required this.errors,
  91.   });
  92. }
复制代码
CSV服务实现
  1. class CsvServiceImpl implements CsvService {
  2.   final BeeRepository repository;
  3.   final Logger logger;
  4.   CsvServiceImpl({
  5.     required this.repository,
  6.     required this.logger,
  7.   });
  8.   @override
  9.   Future<CsvExportResult> exportTransactions({
  10.     required int ledgerId,
  11.     required DateTimeRange dateRange,
  12.     required CsvExportOptions options,
  13.   }) async {
  14.     try {
  15.       logger.info('Starting CSV export for ledger $ledgerId');
  16.       // 获取交易数据
  17.       final transactions = await repository.getTransactionsInRange(
  18.         ledgerId: ledgerId,
  19.         range: dateRange,
  20.         filter: options.filter,
  21.       );
  22.       if (transactions.isEmpty) {
  23.         return CsvExportResult.failure('没有找到符合条件的交易记录');
  24.       }
  25.       // 生成CSV内容
  26.       final csvContent = await _generateCsvContent(
  27.         transactions,
  28.         options,
  29.       );
  30.       // 保存文件
  31.       final filePath = await _saveCsvFile(
  32.         csvContent,
  33.         'transactions_export_${DateTime.now().millisecondsSinceEpoch}.csv',
  34.       );
  35.       logger.info('CSV export completed: ${transactions.length} records');
  36.       return CsvExportResult.success(
  37.         filePath: filePath,
  38.         recordCount: transactions.length,
  39.       );
  40.     } catch (e, stackTrace) {
  41.       logger.error('CSV export failed', e, stackTrace);
  42.       return CsvExportResult.failure('导出失败: $e');
  43.     }
  44.   }
  45.   @override
  46.   Future<CsvImportResult> importTransactions({
  47.     required String csvContent,
  48.     required int ledgerId,
  49.     required CsvImportOptions options,
  50.     void Function(double progress)? onProgress,
  51.   }) async {
  52.     try {
  53.       logger.info('Starting CSV import for ledger $ledgerId');
  54.       // 解析CSV内容
  55.       final List<List<String>> rows = await _parseCsvContent(
  56.         csvContent,
  57.         options,
  58.       );
  59.       if (rows.isEmpty) {
  60.         return CsvImportResult(
  61.           success: false,
  62.           totalRows: 0,
  63.           importedRows: 0,
  64.           skippedRows: 0,
  65.           errors: [ImportError(row: 0, message: 'CSV文件为空')],
  66.         );
  67.       }
  68.       // 处理表头
  69.       int startRow = options.hasHeader ? 1 : 0;
  70.       final dataRows = rows.skip(startRow).toList();
  71.       // 批量导入
  72.       final result = await _importRows(
  73.         dataRows,
  74.         ledgerId,
  75.         options,
  76.         onProgress,
  77.       );
  78.       logger.info('CSV import completed: ${result.importedRows}/${result.totalRows}');
  79.       return result;
  80.     } catch (e, stackTrace) {
  81.       logger.error('CSV import failed', e, stackTrace);
  82.       return CsvImportResult(
  83.         success: false,
  84.         totalRows: 0,
  85.         importedRows: 0,
  86.         skippedRows: 0,
  87.         errors: [ImportError(row: 0, message: '导入失败: $e')],
  88.       );
  89.     }
  90.   }
  91.   Future<String> _generateCsvContent(
  92.     List<TransactionWithDetails> transactions,
  93.     CsvExportOptions options,
  94.   ) async {
  95.     final StringBuffer buffer = StringBuffer();
  96.    
  97.     // 添加表头
  98.     if (options.includeHeader) {
  99.       final headers = options.columns.map((col) => _getColumnDisplayName(col));
  100.       buffer.writeln(headers.join(options.separator));
  101.     }
  102.     // 添加数据行
  103.     for (final transaction in transactions) {
  104.       final row = options.columns.map((column) =>
  105.         _formatCellValue(_getTransactionValue(transaction, column))
  106.       );
  107.       buffer.writeln(row.join(options.separator));
  108.     }
  109.     return buffer.toString();
  110.   }
  111.   String _getTransactionValue(TransactionWithDetails transaction, String column) {
  112.     switch (column) {
  113.       case 'date':
  114.         return DateFormat('yyyy-MM-dd').format(transaction.happenedAt);
  115.       case 'time':
  116.         return DateFormat('HH:mm:ss').format(transaction.happenedAt);
  117.       case 'type':
  118.         return _getTypeDisplayName(transaction.type);
  119.       case 'amount':
  120.         return transaction.amount.toStringAsFixed(2);
  121.       case 'category':
  122.         return transaction.categoryName ?? '';
  123.       case 'account':
  124.         return transaction.accountName ?? '';
  125.       case 'toAccount':
  126.         return transaction.toAccountName ?? '';
  127.       case 'note':
  128.         return transaction.note ?? '';
  129.       default:
  130.         return '';
  131.     }
  132.   }
  133.   String _formatCellValue(String value) {
  134.     // 处理包含逗号、引号、换行符的值
  135.     if (value.contains(',') || value.contains('"') || value.contains('\n')) {
  136.       return '"${value.replaceAll('"', '""')}"';
  137.     }
  138.     return value;
  139.   }
  140.   Future<String> _saveCsvFile(String content, String fileName) async {
  141.     final directory = await getApplicationDocumentsDirectory();
  142.     final file = File(path.join(directory.path, fileName));
  143.     await file.writeAsString(content, encoding: utf8);
  144.     return file.path;
  145.   }
  146.   Future<List<List<String>>> _parseCsvContent(
  147.     String content,
  148.     CsvImportOptions options,
  149.   ) async {
  150.     // 使用csv包解析内容
  151.     return const CsvToListConverter(
  152.       fieldDelimiter: ',',
  153.       textDelimiter: '"',
  154.       eol: '\n',
  155.     ).convert(content);
  156.   }
  157.   Future<CsvImportResult> _importRows(
  158.     List<List<String>> rows,
  159.     int ledgerId,
  160.     CsvImportOptions options,
  161.     void Function(double progress)? onProgress,
  162.   ) async {
  163.     int importedCount = 0;
  164.     int skippedCount = 0;
  165.     final List<ImportError> errors = [];
  166.     // 批量处理,每次处理100行
  167.     const batchSize = 100;
  168.     final totalRows = rows.length;
  169.     for (int i = 0; i < rows.length; i += batchSize) {
  170.       final batchEnd = math.min(i + batchSize, rows.length);
  171.       final batch = rows.sublist(i, batchEnd);
  172.       final batchResult = await _processBatch(
  173.         batch,
  174.         ledgerId,
  175.         options,
  176.         i, // 起始行号
  177.       );
  178.       importedCount += batchResult.importedRows;
  179.       skippedCount += batchResult.skippedRows;
  180.       errors.addAll(batchResult.errors);
  181.       // 更新进度
  182.       if (onProgress != null) {
  183.         final progress = batchEnd / totalRows;
  184.         onProgress(progress);
  185.       }
  186.       // 让出控制权,避免阻塞UI
  187.       await Future.delayed(const Duration(milliseconds: 10));
  188.     }
  189.     return CsvImportResult(
  190.       success: errors.isEmpty || importedCount > 0,
  191.       totalRows: totalRows,
  192.       importedRows: importedCount,
  193.       skippedRows: skippedCount,
  194.       errors: errors,
  195.     );
  196.   }
  197.   Future<BatchImportResult> _processBatch(
  198.     List<List<String>> batch,
  199.     int ledgerId,
  200.     CsvImportOptions options,
  201.     int startRowIndex,
  202.   ) async {
  203.     int importedCount = 0;
  204.     int skippedCount = 0;
  205.     final List<ImportError> errors = [];
  206.     final List<Transaction> transactionsToInsert = [];
  207.     for (int i = 0; i < batch.length; i++) {
  208.       final rowIndex = startRowIndex + i;
  209.       final row = batch[i];
  210.       try {
  211.         final transaction = _parseTransactionFromRow(
  212.           row,
  213.           ledgerId,
  214.           options.columnMapping,
  215.           rowIndex,
  216.         );
  217.         if (transaction != null) {
  218.           // 检查是否跳过重复项
  219.           if (options.skipDuplicates) {
  220.             final exists = await repository.checkTransactionExists(
  221.               ledgerId: ledgerId,
  222.               amount: transaction.amount,
  223.               happenedAt: transaction.happenedAt,
  224.               note: transaction.note,
  225.             );
  226.             if (exists) {
  227.               skippedCount++;
  228.               continue;
  229.             }
  230.           }
  231.           transactionsToInsert.add(transaction);
  232.           importedCount++;
  233.         } else {
  234.           skippedCount++;
  235.         }
  236.       } catch (e) {
  237.         errors.add(ImportError(
  238.           row: rowIndex + 1,
  239.           message: e.toString(),
  240.         ));
  241.         skippedCount++;
  242.       }
  243.     }
  244.     // 批量插入交易
  245.     if (transactionsToInsert.isNotEmpty) {
  246.       await repository.insertTransactionsBatch(transactionsToInsert);
  247.     }
  248.     return BatchImportResult(
  249.       importedRows: importedCount,
  250.       skippedRows: skippedCount,
  251.       errors: errors,
  252.     );
  253.   }
  254.   Transaction? _parseTransactionFromRow(
  255.     List<String> row,
  256.     int ledgerId,
  257.     Map<String, String> columnMapping,
  258.     int rowIndex,
  259.   ) {
  260.     try {
  261.       // 解析必需字段
  262.       final dateStr = _getColumnValue(row, columnMapping, 'date');
  263.       final amountStr = _getColumnValue(row, columnMapping, 'amount');
  264.       final typeStr = _getColumnValue(row, columnMapping, 'type');
  265.       if (dateStr.isEmpty || amountStr.isEmpty || typeStr.isEmpty) {
  266.         throw Exception('缺少必需字段:日期、金额或类型');
  267.       }
  268.       // 解析日期
  269.       final date = _parseDate(dateStr);
  270.       if (date == null) {
  271.         throw Exception('日期格式不正确:$dateStr');
  272.       }
  273.       // 解析金额
  274.       final amount = double.tryParse(amountStr);
  275.       if (amount == null || amount <= 0) {
  276.         throw Exception('金额格式不正确:$amountStr');
  277.       }
  278.       // 解析类型
  279.       final type = _parseTransactionType(typeStr);
  280.       if (type == null) {
  281.         throw Exception('交易类型不支持:$typeStr');
  282.       }
  283.       // 解析可选字段
  284.       final note = _getColumnValue(row, columnMapping, 'note');
  285.       final categoryName = _getColumnValue(row, columnMapping, 'category');
  286.       final accountName = _getColumnValue(row, columnMapping, 'account');
  287.       return Transaction(
  288.         id: 0, // 将由数据库自动分配
  289.         ledgerId: ledgerId,
  290.         type: type,
  291.         amount: amount,
  292.         categoryId: await _getCategoryId(ledgerId, categoryName, type),
  293.         accountId: await _getAccountId(ledgerId, accountName),
  294.         happenedAt: date,
  295.         note: note.isEmpty ? null : note,
  296.       );
  297.     } catch (e) {
  298.       logger.warning('Failed to parse row $rowIndex: $e');
  299.       return null;
  300.     }
  301.   }
  302.   String _getColumnValue(List<String> row, Map<String, String> mapping, String logicalColumn) {
  303.     final physicalColumn = mapping[logicalColumn];
  304.     if (physicalColumn == null) return '';
  305.     final columnIndex = int.tryParse(physicalColumn);
  306.     if (columnIndex == null || columnIndex >= row.length) return '';
  307.     return row[columnIndex].trim();
  308.   }
  309.   DateTime? _parseDate(String dateStr) {
  310.     // 尝试多种日期格式
  311.     final formats = [
  312.       'yyyy-MM-dd',
  313.       'yyyy/MM/dd',
  314.       'MM/dd/yyyy',
  315.       'dd/MM/yyyy',
  316.       'yyyy-MM-dd HH:mm:ss',
  317.       'MM/dd/yyyy HH:mm:ss',
  318.     ];
  319.     for (final format in formats) {
  320.       try {
  321.         return DateFormat(format).parse(dateStr);
  322.       } catch (_) {
  323.         continue;
  324.       }
  325.     }
  326.     return null;
  327.   }
  328.   String? _parseTransactionType(String typeStr) {
  329.     final type = typeStr.toLowerCase();
  330.     switch (type) {
  331.       case '支出':
  332.       case 'expense':
  333.       case '出':
  334.       case '-':
  335.         return 'expense';
  336.       case '收入':
  337.       case 'income':
  338.       case '入':
  339.       case '+':
  340.         return 'income';
  341.       case '转账':
  342.       case 'transfer':
  343.       case '转':
  344.         return 'transfer';
  345.       default:
  346.         return null;
  347.     }
  348.   }
  349. }
复制代码
文件选择与预览

文件选择器实现
  1. class CsvImportPage extends ConsumerStatefulWidget {
  2.   const CsvImportPage({Key? key}) : super(key: key);
  3.   @override
  4.   ConsumerState<CsvImportPage> createState() => _CsvImportPageState();
  5. }
  6. class _CsvImportPageState extends ConsumerState<CsvImportPage> {
  7.   String? _selectedFilePath;
  8.   List<List<String>>? _previewData;
  9.   CsvImportOptions? _importOptions;
  10.   bool _isAnalyzing = false;
  11.   bool _isImporting = false;
  12.   double _importProgress = 0.0;
  13.   @override
  14.   Widget build(BuildContext context) {
  15.     return Scaffold(
  16.       appBar: AppBar(
  17.         title: const Text('导入CSV'),
  18.         actions: [
  19.           if (_previewData != null && _importOptions != null)
  20.             TextButton(
  21.               onPressed: _isImporting ? null : _startImport,
  22.               child: const Text('导入'),
  23.             ),
  24.         ],
  25.       ),
  26.       body: Column(
  27.         children: [
  28.           if (_isImporting) _buildProgressIndicator(),
  29.           Expanded(
  30.             child: _selectedFilePath == null
  31.                 ? _buildFilePicker()
  32.                 : _buildPreviewAndMapping(),
  33.           ),
  34.         ],
  35.       ),
  36.     );
  37.   }
  38.   Widget _buildFilePicker() {
  39.     return Center(
  40.       child: Column(
  41.         mainAxisAlignment: MainAxisAlignment.center,
  42.         children: [
  43.           Icon(
  44.             Icons.upload_file,
  45.             size: 80,
  46.             color: Theme.of(context).colorScheme.primary.withOpacity(0.6),
  47.           ),
  48.           const SizedBox(height: 24),
  49.          
  50.           Text(
  51.             '选择CSV文件',
  52.             style: Theme.of(context).textTheme.headlineSmall,
  53.           ),
  54.           const SizedBox(height: 8),
  55.          
  56.           Text(
  57.             '支持从其他记账应用导出的CSV文件',
  58.             style: Theme.of(context).textTheme.bodyMedium?.copyWith(
  59.               color: Theme.of(context).colorScheme.onSurfaceVariant,
  60.             ),
  61.             textAlign: TextAlign.center,
  62.           ),
  63.           const SizedBox(height: 32),
  64.          
  65.           FilledButton.icon(
  66.             onPressed: _pickFile,
  67.             icon: const Icon(Icons.folder_open),
  68.             label: const Text('选择文件'),
  69.           ),
  70.          
  71.           const SizedBox(height: 16),
  72.           TextButton.icon(
  73.             onPressed: _showImportGuide,
  74.             icon: const Icon(Icons.help_outline),
  75.             label: const Text('导入说明'),
  76.           ),
  77.         ],
  78.       ),
  79.     );
  80.   }
  81.   Widget _buildPreviewAndMapping() {
  82.     return Column(
  83.       children: [
  84.         // 文件信息
  85.         Card(
  86.           margin: const EdgeInsets.all(16),
  87.           child: ListTile(
  88.             leading: const Icon(Icons.description),
  89.             title: Text(path.basename(_selectedFilePath!)),
  90.             subtitle: Text('${_previewData?.length ?? 0} 行数据'),
  91.             trailing: IconButton(
  92.               onPressed: _clearSelection,
  93.               icon: const Icon(Icons.close),
  94.             ),
  95.           ),
  96.         ),
  97.         // 预览和字段映射
  98.         Expanded(
  99.           child: DefaultTabController(
  100.             length: 2,
  101.             child: Column(
  102.               children: [
  103.                 const TabBar(
  104.                   tabs: [
  105.                     Tab(text: '数据预览'),
  106.                     Tab(text: '字段映射'),
  107.                   ],
  108.                 ),
  109.                 Expanded(
  110.                   child: TabBarView(
  111.                     children: [
  112.                       _buildDataPreview(),
  113.                       _buildFieldMapping(),
  114.                     ],
  115.                   ),
  116.                 ),
  117.               ],
  118.             ),
  119.           ),
  120.         ),
  121.       ],
  122.     );
  123.   }
  124.   Widget _buildDataPreview() {
  125.     if (_previewData == null || _previewData!.isEmpty) {
  126.       return const Center(child: Text('无数据可预览'));
  127.     }
  128.     // 只显示前10行数据
  129.     final previewRows = _previewData!.take(10).toList();
  130.     return SingleChildScrollView(
  131.       padding: const EdgeInsets.all(16),
  132.       child: SingleChildScrollView(
  133.         scrollDirection: Axis.horizontal,
  134.         child: DataTable(
  135.           columnSpacing: 20,
  136.           columns: previewRows.first.asMap().entries.map((entry) {
  137.             return DataColumn(
  138.               label: Text(
  139.                 '列 ${entry.key + 1}',
  140.                 style: Theme.of(context).textTheme.bodySmall,
  141.               ),
  142.             );
  143.           }).toList(),
  144.           rows: previewRows.skip(1).map((row) {
  145.             return DataRow(
  146.               cells: row.map((cell) {
  147.                 return DataCell(
  148.                   Container(
  149.                     constraints: const BoxConstraints(maxWidth: 120),
  150.                     child: Text(
  151.                       cell,
  152.                       overflow: TextOverflow.ellipsis,
  153.                       style: Theme.of(context).textTheme.bodySmall,
  154.                     ),
  155.                   ),
  156.                 );
  157.               }).toList(),
  158.             );
  159.           }).toList(),
  160.         ),
  161.       ),
  162.     );
  163.   }
  164.   Widget _buildFieldMapping() {
  165.     if (_previewData == null || _previewData!.isEmpty) {
  166.       return const Center(child: Text('无数据可映射'));
  167.     }
  168.     final headers = _previewData!.first;
  169.    
  170.     return SingleChildScrollView(
  171.       padding: const EdgeInsets.all(16),
  172.       child: Column(
  173.         crossAxisAlignment: CrossAxisAlignment.start,
  174.         children: [
  175.           Text(
  176.             '字段映射',
  177.             style: Theme.of(context).textTheme.titleMedium,
  178.           ),
  179.           const SizedBox(height: 8),
  180.           Text(
  181.             '请将CSV文件的列映射到对应的交易字段',
  182.             style: Theme.of(context).textTheme.bodyMedium?.copyWith(
  183.               color: Theme.of(context).colorScheme.onSurfaceVariant,
  184.             ),
  185.           ),
  186.           const SizedBox(height: 16),
  187.           ...{
  188.             'date': '日期 *',
  189.             'amount': '金额 *',
  190.             'type': '类型 *',
  191.             'category': '分类',
  192.             'account': '账户',
  193.             'note': '备注',
  194.           }.entries.map((entry) {
  195.             return _buildFieldMappingRow(
  196.               entry.key,
  197.               entry.value,
  198.               headers,
  199.             );
  200.           }).toList(),
  201.           const SizedBox(height: 24),
  202.          
  203.           // 导入选项
  204.           _buildImportOptions(),
  205.         ],
  206.       ),
  207.     );
  208.   }
  209.   Widget _buildFieldMappingRow(
  210.     String field,
  211.     String displayName,
  212.     List<String> headers,
  213.   ) {
  214.     return Padding(
  215.       padding: const EdgeInsets.symmetric(vertical: 8),
  216.       child: Row(
  217.         children: [
  218.           SizedBox(
  219.             width: 100,
  220.             child: Text(
  221.               displayName,
  222.               style: Theme.of(context).textTheme.bodyMedium?.copyWith(
  223.                 fontWeight: displayName.contains('*')
  224.                     ? FontWeight.w600
  225.                     : FontWeight.normal,
  226.               ),
  227.             ),
  228.           ),
  229.           Expanded(
  230.             child: DropdownButtonFormField<String>(
  231.               value: _importOptions?.columnMapping[field],
  232.               decoration: const InputDecoration(
  233.                 border: OutlineInputBorder(),
  234.                 contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
  235.               ),
  236.               hint: const Text('选择列'),
  237.               items: [
  238.                 const DropdownMenuItem<String>(
  239.                   value: null,
  240.                   child: Text('不映射'),
  241.                 ),
  242.                 ...headers.asMap().entries.map((entry) {
  243.                   return DropdownMenuItem<String>(
  244.                     value: entry.key.toString(),
  245.                     child: Text('列${entry.key + 1}: ${entry.value}'),
  246.                   );
  247.                 }),
  248.               ],
  249.               onChanged: (value) {
  250.                 _updateFieldMapping(field, value);
  251.               },
  252.             ),
  253.           ),
  254.         ],
  255.       ),
  256.     );
  257.   }
  258.   Widget _buildImportOptions() {
  259.     return Column(
  260.       crossAxisAlignment: CrossAxisAlignment.start,
  261.       children: [
  262.         Text(
  263.           '导入选项',
  264.           style: Theme.of(context).textTheme.titleMedium,
  265.         ),
  266.         const SizedBox(height: 16),
  267.         SwitchListTile(
  268.           title: const Text('第一行为标题行'),
  269.           subtitle: const Text('勾选则跳过第一行数据'),
  270.           value: _importOptions?.hasHeader ?? true,
  271.           onChanged: (value) {
  272.             _updateImportOption('hasHeader', value);
  273.           },
  274.         ),
  275.         SwitchListTile(
  276.           title: const Text('跳过重复记录'),
  277.           subtitle: const Text('根据金额、日期和备注判断重复'),
  278.           value: _importOptions?.skipDuplicates ?? true,
  279.           onChanged: (value) {
  280.             _updateImportOption('skipDuplicates', value);
  281.           },
  282.         ),
  283.       ],
  284.     );
  285.   }
  286.   Widget _buildProgressIndicator() {
  287.     return Container(
  288.       padding: const EdgeInsets.all(16),
  289.       child: Column(
  290.         children: [
  291.           LinearProgressIndicator(value: _importProgress),
  292.           const SizedBox(height: 8),
  293.           Text(
  294.             '导入进度: ${(_importProgress * 100).toInt()}%',
  295.             style: Theme.of(context).textTheme.bodySmall,
  296.           ),
  297.         ],
  298.       ),
  299.     );
  300.   }
  301.   Future<void> _pickFile() async {
  302.     try {
  303.       final result = await FilePicker.platform.pickFiles(
  304.         type: FileType.custom,
  305.         allowedExtensions: ['csv'],
  306.         allowMultiple: false,
  307.       );
  308.       if (result != null && result.files.isNotEmpty) {
  309.         final file = result.files.first;
  310.         if (file.path != null) {
  311.           setState(() {
  312.             _selectedFilePath = file.path;
  313.             _isAnalyzing = true;
  314.           });
  315.           await _analyzeCsvFile();
  316.         }
  317.       }
  318.     } catch (e) {
  319.       _showErrorDialog('文件选择失败: $e');
  320.     }
  321.   }
  322.   Future<void> _analyzeCsvFile() async {
  323.     try {
  324.       final file = File(_selectedFilePath!);
  325.       final content = await file.readAsString();
  326.       
  327.       // 解析CSV预览数据
  328.       final rows = const CsvToListConverter().convert(content);
  329.       
  330.       setState(() {
  331.         _previewData = rows;
  332.         _importOptions = CsvImportOptions(
  333.           hasHeader: true,
  334.           skipDuplicates: true,
  335.           columnMapping: {},
  336.           conflictResolution: ConflictResolution.skip,
  337.         );
  338.         _isAnalyzing = false;
  339.       });
  340.       // 尝试智能映射字段
  341.       _attemptAutoMapping();
  342.     } catch (e) {
  343.       setState(() {
  344.         _isAnalyzing = false;
  345.       });
  346.       _showErrorDialog('文件解析失败: $e');
  347.     }
  348.   }
  349.   void _attemptAutoMapping() {
  350.     if (_previewData == null || _previewData!.isEmpty) return;
  351.     final headers = _previewData!.first.map((h) => h.toLowerCase()).toList();
  352.     final Map<String, String> autoMapping = {};
  353.     // 智能匹配字段
  354.     for (int i = 0; i < headers.length; i++) {
  355.       final header = headers[i];
  356.       
  357.       if (header.contains('日期') || header.contains('date') || header.contains('time')) {
  358.         autoMapping['date'] = i.toString();
  359.       } else if (header.contains('金额') || header.contains('amount') || header.contains('money')) {
  360.         autoMapping['amount'] = i.toString();
  361.       } else if (header.contains('类型') || header.contains('type') || header.contains('kind')) {
  362.         autoMapping['type'] = i.toString();
  363.       } else if (header.contains('分类') || header.contains('category')) {
  364.         autoMapping['category'] = i.toString();
  365.       } else if (header.contains('账户') || header.contains('account')) {
  366.         autoMapping['account'] = i.toString();
  367.       } else if (header.contains('备注') || header.contains('note') || header.contains('memo')) {
  368.         autoMapping['note'] = i.toString();
  369.       }
  370.     }
  371.     setState(() {
  372.       _importOptions = _importOptions!.copyWith(columnMapping: autoMapping);
  373.     });
  374.   }
  375.   void _updateFieldMapping(String field, String? columnIndex) {
  376.     final newMapping = Map<String, String>.from(_importOptions!.columnMapping);
  377.     if (columnIndex != null) {
  378.       newMapping[field] = columnIndex;
  379.     } else {
  380.       newMapping.remove(field);
  381.     }
  382.     setState(() {
  383.       _importOptions = _importOptions!.copyWith(columnMapping: newMapping);
  384.     });
  385.   }
  386.   void _updateImportOption(String option, dynamic value) {
  387.     setState(() {
  388.       switch (option) {
  389.         case 'hasHeader':
  390.           _importOptions = _importOptions!.copyWith(hasHeader: value);
  391.           break;
  392.         case 'skipDuplicates':
  393.           _importOptions = _importOptions!.copyWith(skipDuplicates: value);
  394.           break;
  395.       }
  396.     });
  397.   }
  398.   Future<void> _startImport() async {
  399.     // 验证必需字段
  400.     final requiredFields = ['date', 'amount', 'type'];
  401.     final missingFields = requiredFields.where(
  402.       (field) => !_importOptions!.columnMapping.containsKey(field),
  403.     ).toList();
  404.     if (missingFields.isNotEmpty) {
  405.       _showErrorDialog('请映射必需字段: ${missingFields.join(', ')}');
  406.       return;
  407.     }
  408.     setState(() {
  409.       _isImporting = true;
  410.       _importProgress = 0.0;
  411.     });
  412.     try {
  413.       final csvService = ref.read(csvServiceProvider);
  414.       final currentLedgerId = ref.read(currentLedgerIdProvider);
  415.       final file = File(_selectedFilePath!);
  416.       final content = await file.readAsString();
  417.       final result = await csvService.importTransactions(
  418.         csvContent: content,
  419.         ledgerId: currentLedgerId,
  420.         options: _importOptions!,
  421.         onProgress: (progress) {
  422.           setState(() {
  423.             _importProgress = progress;
  424.           });
  425.         },
  426.       );
  427.       setState(() {
  428.         _isImporting = false;
  429.       });
  430.       _showImportResult(result);
  431.     } catch (e) {
  432.       setState(() {
  433.         _isImporting = false;
  434.       });
  435.       _showErrorDialog('导入失败: $e');
  436.     }
  437.   }
  438.   void _showImportResult(CsvImportResult result) {
  439.     showDialog(
  440.       context: context,
  441.       builder: (context) => ImportResultDialog(result: result),
  442.     );
  443.   }
  444.   void _showErrorDialog(String message) {
  445.     showDialog(
  446.       context: context,
  447.       builder: (context) => AlertDialog(
  448.         title: const Text('错误'),
  449.         content: Text(message),
  450.         actions: [
  451.           TextButton(
  452.             onPressed: () => Navigator.pop(context),
  453.             child: const Text('确定'),
  454.           ),
  455.         ],
  456.       ),
  457.     );
  458.   }
  459.   void _clearSelection() {
  460.     setState(() {
  461.       _selectedFilePath = null;
  462.       _previewData = null;
  463.       _importOptions = null;
  464.     });
  465.   }
  466.   void _showImportGuide() {
  467.     showDialog(
  468.       context: context,
  469.       builder: (context) => const ImportGuideDialog(),
  470.     );
  471.   }
  472. }
复制代码
导出功能实现

导出选项配置
  1. class CsvExportPage extends ConsumerStatefulWidget {
  2.   const CsvExportPage({Key? key}) : super(key: key);
  3.   @override
  4.   ConsumerState<CsvExportPage> createState() => _CsvExportPageState();
  5. }
  6. class _CsvExportPageState extends ConsumerState<CsvExportPage> {
  7.   DateTimeRange _dateRange = DateTimeRange(
  8.     start: DateTime.now().subtract(const Duration(days: 30)),
  9.     end: DateTime.now(),
  10.   );
  11.   
  12.   final Set<String> _selectedColumns = {
  13.     'date',
  14.     'type',
  15.     'amount',
  16.     'category',
  17.     'account',
  18.     'note',
  19.   };
  20.   
  21.   bool _isExporting = false;
  22.   @override
  23.   Widget build(BuildContext context) {
  24.     return Scaffold(
  25.       appBar: AppBar(
  26.         title: const Text('导出CSV'),
  27.         actions: [
  28.           TextButton(
  29.             onPressed: _isExporting ? null : _startExport,
  30.             child: const Text('导出'),
  31.           ),
  32.         ],
  33.       ),
  34.       body: SingleChildScrollView(
  35.         padding: const EdgeInsets.all(16),
  36.         child: Column(
  37.           crossAxisAlignment: CrossAxisAlignment.start,
  38.           children: [
  39.             _buildDateRangeSelector(),
  40.             const SizedBox(height: 24),
  41.             _buildColumnSelector(),
  42.             const SizedBox(height: 24),
  43.             _buildExportOptions(),
  44.           ],
  45.         ),
  46.       ),
  47.     );
  48.   }
  49.   Widget _buildDateRangeSelector() {
  50.     return Card(
  51.       child: Padding(
  52.         padding: const EdgeInsets.all(16),
  53.         child: Column(
  54.           crossAxisAlignment: CrossAxisAlignment.start,
  55.           children: [
  56.             Text(
  57.               '选择时间范围',
  58.               style: Theme.of(context).textTheme.titleMedium,
  59.             ),
  60.             const SizedBox(height: 16),
  61.             
  62.             Row(
  63.               children: [
  64.                 Expanded(
  65.                   child: _buildDateButton(
  66.                     '开始日期',
  67.                     _dateRange.start,
  68.                     (date) {
  69.                       setState(() {
  70.                         _dateRange = DateTimeRange(
  71.                           start: date,
  72.                           end: _dateRange.end,
  73.                         );
  74.                       });
  75.                     },
  76.                   ),
  77.                 ),
  78.                 const SizedBox(width: 16),
  79.                 Expanded(
  80.                   child: _buildDateButton(
  81.                     '结束日期',
  82.                     _dateRange.end,
  83.                     (date) {
  84.                       setState(() {
  85.                         _dateRange = DateTimeRange(
  86.                           start: _dateRange.start,
  87.                           end: date,
  88.                         );
  89.                       });
  90.                     },
  91.                   ),
  92.                 ),
  93.               ],
  94.             ),
  95.             const SizedBox(height: 16),
  96.             
  97.             // 快捷选择按钮
  98.             Wrap(
  99.               spacing: 8,
  100.               children: [
  101.                 _buildQuickRangeChip('最近7天', 7),
  102.                 _buildQuickRangeChip('最近30天', 30),
  103.                 _buildQuickRangeChip('最近90天', 90),
  104.                 _buildQuickRangeChip('本年', 365),
  105.               ],
  106.             ),
  107.           ],
  108.         ),
  109.       ),
  110.     );
  111.   }
  112.   Widget _buildDateButton(String label, DateTime date, Function(DateTime) onSelected) {
  113.     return OutlinedButton(
  114.       onPressed: () async {
  115.         final selected = await showDatePicker(
  116.           context: context,
  117.           initialDate: date,
  118.           firstDate: DateTime(2020),
  119.           lastDate: DateTime.now(),
  120.         );
  121.         if (selected != null) {
  122.           onSelected(selected);
  123.         }
  124.       },
  125.       child: Column(
  126.         mainAxisSize: MainAxisSize.min,
  127.         children: [
  128.           Text(
  129.             label,
  130.             style: Theme.of(context).textTheme.bodySmall,
  131.           ),
  132.           const SizedBox(height: 4),
  133.           Text(
  134.             DateFormat('yyyy-MM-dd').format(date),
  135.             style: Theme.of(context).textTheme.bodyLarge,
  136.           ),
  137.         ],
  138.       ),
  139.     );
  140.   }
  141.   Widget _buildQuickRangeChip(String label, int days) {
  142.     return ActionChip(
  143.       label: Text(label),
  144.       onPressed: () {
  145.         setState(() {
  146.           _dateRange = DateTimeRange(
  147.             start: DateTime.now().subtract(Duration(days: days)),
  148.             end: DateTime.now(),
  149.           );
  150.         });
  151.       },
  152.     );
  153.   }
  154.   Widget _buildColumnSelector() {
  155.     final availableColumns = {
  156.       'date': '日期',
  157.       'time': '时间',
  158.       'type': '类型',
  159.       'amount': '金额',
  160.       'category': '分类',
  161.       'account': '账户',
  162.       'toAccount': '转入账户',
  163.       'note': '备注',
  164.     };
  165.     return Card(
  166.       child: Padding(
  167.         padding: const EdgeInsets.all(16),
  168.         child: Column(
  169.           crossAxisAlignment: CrossAxisAlignment.start,
  170.           children: [
  171.             Row(
  172.               mainAxisAlignment: MainAxisAlignment.spaceBetween,
  173.               children: [
  174.                 Text(
  175.                   '选择导出字段',
  176.                   style: Theme.of(context).textTheme.titleMedium,
  177.                 ),
  178.                 Row(
  179.                   children: [
  180.                     TextButton(
  181.                       onPressed: () {
  182.                         setState(() {
  183.                           _selectedColumns.addAll(availableColumns.keys);
  184.                         });
  185.                       },
  186.                       child: const Text('全选'),
  187.                     ),
  188.                     TextButton(
  189.                       onPressed: () {
  190.                         setState(() {
  191.                           _selectedColumns.clear();
  192.                         });
  193.                       },
  194.                       child: const Text('清空'),
  195.                     ),
  196.                   ],
  197.                 ),
  198.               ],
  199.             ),
  200.             const SizedBox(height: 8),
  201.             
  202.             ...availableColumns.entries.map((entry) {
  203.               return CheckboxListTile(
  204.                 title: Text(entry.value),
  205.                 value: _selectedColumns.contains(entry.key),
  206.                 onChanged: (value) {
  207.                   setState(() {
  208.                     if (value ?? false) {
  209.                       _selectedColumns.add(entry.key);
  210.                     } else {
  211.                       _selectedColumns.remove(entry.key);
  212.                     }
  213.                   });
  214.                 },
  215.                 contentPadding: EdgeInsets.zero,
  216.               );
  217.             }).toList(),
  218.           ],
  219.         ),
  220.       ),
  221.     );
  222.   }
  223.   Widget _buildExportOptions() {
  224.     return Card(
  225.       child: Padding(
  226.         padding: const EdgeInsets.all(16),
  227.         child: Column(
  228.           crossAxisAlignment: CrossAxisAlignment.start,
  229.           children: [
  230.             Text(
  231.               '导出选项',
  232.               style: Theme.of(context).textTheme.titleMedium,
  233.             ),
  234.             const SizedBox(height: 16),
  235.             
  236.             SwitchListTile(
  237.               title: const Text('包含表头'),
  238.               subtitle: const Text('在第一行包含字段名称'),
  239.               value: true,
  240.               onChanged: null, // 暂时固定为true
  241.               contentPadding: EdgeInsets.zero,
  242.             ),
  243.             
  244.             ListTile(
  245.               title: const Text('文件格式'),
  246.               subtitle: const Text('UTF-8 编码的CSV文件'),
  247.               trailing: const Text('CSV'),
  248.               contentPadding: EdgeInsets.zero,
  249.             ),
  250.           ],
  251.         ),
  252.       ),
  253.     );
  254.   }
  255.   Future<void> _startExport() async {
  256.     if (_selectedColumns.isEmpty) {
  257.       ScaffoldMessenger.of(context).showSnackBar(
  258.         const SnackBar(content: Text('请选择至少一个导出字段')),
  259.       );
  260.       return;
  261.     }
  262.     setState(() {
  263.       _isExporting = true;
  264.     });
  265.     try {
  266.       final csvService = ref.read(csvServiceProvider);
  267.       final currentLedgerId = ref.read(currentLedgerIdProvider);
  268.       
  269.       final result = await csvService.exportTransactions(
  270.         ledgerId: currentLedgerId,
  271.         dateRange: _dateRange,
  272.         options: CsvExportOptions(
  273.           columns: _selectedColumns.toList(),
  274.           includeHeader: true,
  275.         ),
  276.       );
  277.       setState(() {
  278.         _isExporting = false;
  279.       });
  280.       if (result.success) {
  281.         _showExportSuccess(result);
  282.       } else {
  283.         _showErrorDialog(result.error ?? '导出失败');
  284.       }
  285.     } catch (e) {
  286.       setState(() {
  287.         _isExporting = false;
  288.       });
  289.       _showErrorDialog('导出失败: $e');
  290.     }
  291.   }
  292.   void _showExportSuccess(CsvExportResult result) {
  293.     showDialog(
  294.       context: context,
  295.       builder: (context) => AlertDialog(
  296.         title: const Text('导出成功'),
  297.         content: Column(
  298.           mainAxisSize: MainAxisSize.min,
  299.           crossAxisAlignment: CrossAxisAlignment.start,
  300.           children: [
  301.             Text('已导出 ${result.recordCount} 条记录'),
  302.             const SizedBox(height: 8),
  303.             Text('文件位置: ${result.filePath}'),
  304.           ],
  305.         ),
  306.         actions: [
  307.           TextButton(
  308.             onPressed: () => Navigator.pop(context),
  309.             child: const Text('确定'),
  310.           ),
  311.           FilledButton(
  312.             onPressed: () {
  313.               // 分享文件
  314.               Share.shareFiles([result.filePath!]);
  315.               Navigator.pop(context);
  316.             },
  317.             child: const Text('分享'),
  318.           ),
  319.         ],
  320.       ),
  321.     );
  322.   }
  323.   void _showErrorDialog(String message) {
  324.     showDialog(
  325.       context: context,
  326.       builder: (context) => AlertDialog(
  327.         title: const Text('错误'),
  328.         content: Text(message),
  329.         actions: [
  330.           TextButton(
  331.             onPressed: () => Navigator.pop(context),
  332.             child: const Text('确定'),
  333.           ),
  334.         ],
  335.       ),
  336.     );
  337.   }
  338. }
复制代码
性能优化策略

流式处理大文件
  1. class StreamCsvProcessor {
  2.   static Future<void> processLargeFile({
  3.     required String filePath,
  4.     required Function(List<String> row, int rowIndex) onRow,
  5.     required Function(double progress) onProgress,
  6.   }) async {
  7.     final file = File(filePath);
  8.     final fileLength = await file.length();
  9.     int processedBytes = 0;
  10.     final stream = file.openRead();
  11.     final lines = stream
  12.         .transform(utf8.decoder)
  13.         .transform(const LineSplitter());
  14.     int rowIndex = 0;
  15.     await for (final line in lines) {
  16.       // 解析CSV行
  17.       final row = _parseCsvLine(line);
  18.       
  19.       // 处理行数据
  20.       await onRow(row, rowIndex);
  21.       
  22.       // 更新进度
  23.       processedBytes += line.length + 1; // +1 for newline
  24.       final progress = processedBytes / fileLength;
  25.       onProgress(progress.clamp(0.0, 1.0));
  26.       
  27.       rowIndex++;
  28.       
  29.       // 每处理100行让出一次控制权
  30.       if (rowIndex % 100 == 0) {
  31.         await Future.delayed(const Duration(milliseconds: 1));
  32.       }
  33.     }
  34.   }
  35.   static List<String> _parseCsvLine(String line) {
  36.     // 简化的CSV行解析,实际使用应该用专业的CSV解析器
  37.     final List<String> fields = [];
  38.     bool inQuotes = false;
  39.     StringBuffer currentField = StringBuffer();
  40.    
  41.     for (int i = 0; i < line.length; i++) {
  42.       final char = line[i];
  43.       
  44.       if (char == '"') {
  45.         if (inQuotes && i + 1 < line.length && line[i + 1] == '"') {
  46.           // 转义的引号
  47.           currentField.write('"');
  48.           i++; // 跳过下一个引号
  49.         } else {
  50.           // 切换引号状态
  51.           inQuotes = !inQuotes;
  52.         }
  53.       } else if (char == ',' && !inQuotes) {
  54.         // 字段分隔符
  55.         fields.add(currentField.toString());
  56.         currentField.clear();
  57.       } else {
  58.         currentField.write(char);
  59.       }
  60.     }
  61.    
  62.     // 添加最后一个字段
  63.     fields.add(currentField.toString());
  64.    
  65.     return fields;
  66.   }
  67. }
复制代码
内存使用优化
  1. class MemoryEfficientCsvImporter {
  2.   static const int _batchSize = 100;
  3.   static const int _maxMemoryRows = 1000;
  4.   static Future<CsvImportResult> importWithMemoryLimit({
  5.     required String csvContent,
  6.     required Function(List<Transaction>) onBatch,
  7.     required Function(double progress) onProgress,
  8.   }) async {
  9.     int totalRows = 0;
  10.     int importedRows = 0;
  11.     final List<ImportError> errors = [];
  12.    
  13.     // 分块处理CSV内容
  14.     final chunks = _splitIntoChunks(csvContent, _maxMemoryRows);
  15.    
  16.     for (int chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
  17.       final chunk = chunks[chunkIndex];
  18.       final chunkResult = await _processChunk(
  19.         chunk,
  20.         totalRows,
  21.         onBatch,
  22.       );
  23.       
  24.       totalRows += chunkResult.totalRows;
  25.       importedRows += chunkResult.importedRows;
  26.       errors.addAll(chunkResult.errors);
  27.       
  28.       // 更新进度
  29.       final progress = (chunkIndex + 1) / chunks.length;
  30.       onProgress(progress);
  31.       
  32.       // 强制垃圾回收
  33.       if (chunkIndex % 5 == 0) {
  34.         await _forceGarbageCollection();
  35.       }
  36.     }
  37.    
  38.     return CsvImportResult(
  39.       success: errors.isEmpty || importedRows > 0,
  40.       totalRows: totalRows,
  41.       importedRows: importedRows,
  42.       skippedRows: totalRows - importedRows,
  43.       errors: errors,
  44.     );
  45.   }
  46.   static List<String> _splitIntoChunks(String content, int maxRowsPerChunk) {
  47.     final lines = content.split('\n');
  48.     final List<String> chunks = [];
  49.    
  50.     for (int i = 0; i < lines.length; i += maxRowsPerChunk) {
  51.       final end = math.min(i + maxRowsPerChunk, lines.length);
  52.       final chunkLines = lines.sublist(i, end);
  53.       chunks.add(chunkLines.join('\n'));
  54.     }
  55.    
  56.     return chunks;
  57.   }
  58.   static Future<ChunkImportResult> _processChunk(
  59.     String chunk,
  60.     int startRowIndex,
  61.     Function(List<Transaction>) onBatch,
  62.   ) async {
  63.     // 解析块数据
  64.     final rows = const CsvToListConverter().convert(chunk);
  65.     final List<Transaction> transactions = [];
  66.     final List<ImportError> errors = [];
  67.    
  68.     for (int i = 0; i < rows.length; i++) {
  69.       try {
  70.         final transaction = _parseTransaction(rows[i]);
  71.         if (transaction != null) {
  72.           transactions.add(transaction);
  73.          
  74.           // 达到批次大小时处理
  75.           if (transactions.length >= _batchSize) {
  76.             await onBatch(List.from(transactions));
  77.             transactions.clear();
  78.           }
  79.         }
  80.       } catch (e) {
  81.         errors.add(ImportError(
  82.           row: startRowIndex + i + 1,
  83.           message: e.toString(),
  84.         ));
  85.       }
  86.     }
  87.    
  88.     // 处理剩余交易
  89.     if (transactions.isNotEmpty) {
  90.       await onBatch(transactions);
  91.     }
  92.    
  93.     return ChunkImportResult(
  94.       totalRows: rows.length,
  95.       importedRows: rows.length - errors.length,
  96.       errors: errors,
  97.     );
  98.   }
  99.   static Future<void> _forceGarbageCollection() async {
  100.     // 触发垃圾回收的技巧
  101.     final List<List<int>> dummy = [];
  102.     for (int i = 0; i < 100; i++) {
  103.       dummy.add(List.filled(1000, i));
  104.     }
  105.     dummy.clear();
  106.    
  107.     // 让出控制权,给垃圾回收器时间
  108.     await Future.delayed(const Duration(milliseconds: 10));
  109.   }
  110. }
复制代码
最佳实践总结

1. 文件处理原则


  • 分批处理:大文件分批处理,避免内存溢出
  • 流式处理:使用流式读取处理超大文件
  • 错误恢复:提供重试和断点续传机制
2. 用户体验优化


  • 进度反馈:实时显示处理进度
  • 错误提示:清晰的错误信息和解决建议
  • 智能映射:自动识别和映射常见字段
3. 数据验证


  • 格式验证:严格验证数据格式和类型
  • 业务验证:检查数据的业务逻辑正确性
  • 重复检测:提供重复数据检测和处理选项
4. 性能考虑


  • 内存管理:控制内存使用,及时释放资源
  • 并发限制:限制并发操作数量
  • 缓存策略:合理使用缓存提升性能
实际应用效果

在BeeCount项目中,CSV导入导出功能带来了显著价值:

  • 用户迁移便利:支持从其他记账应用快速迁移数据
  • 数据安全保障:提供本地数据备份和恢复能力
  • 分析能力增强:导出数据进行深度分析
  • 用户满意度提升:解决了数据互操作性问题
结语

CSV数据处理是移动应用的重要功能,需要在功能完整性、性能效率和用户体验之间找到平衡。通过合理的架构设计、性能优化和用户体验考虑,我们可以构建出既强大又易用的数据处理系统。
BeeCount的实践证明,优秀的CSV处理功能不仅能解决用户的实际需求,还能提升应用的专业性和竞争力,为用户提供真正的价值。
关于BeeCount项目

项目特色

<ul>
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册