import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import '../../../core/network/api_exception.dart'; import '../../../providers/ocr_provider.dart'; import '../../../utils/toast.dart'; import 'ocr_result_card.dart'; /// OCR 识别 Tab:选图 → 识别 → 结果列表(复刻 uniApp add.vue OCR Tab) class OcrTab extends ConsumerWidget { const OcrTab({super.key}); Future _chooseImage(BuildContext context, WidgetRef ref, ImageSource source) async { final picker = ImagePicker(); final xfile = await picker.pickImage(source: source, imageQuality: 80, maxWidth: 1600); if (xfile != null) { ref.read(ocrProvider.notifier).setImagePath(xfile.path); } } Future _recognize(BuildContext context, WidgetRef ref) async { try { await ref.read(ocrProvider.notifier).recognize(); final count = ref.read(ocrProvider).results.length; if (context.mounted) { showToast(context, count > 0 ? '识别到 $count 条记录' : '未识别到账单信息', error: count == 0); } } on ApiException catch (e) { if (context.mounted) showToast(context, e.message, error: true); } catch (_) { if (context.mounted) showToast(context, '识别失败,请重试', error: true); } } Future _saveDirectly(BuildContext context, WidgetRef ref) async { try { await ref.read(ocrProvider.notifier).saveDirectly(); if (context.mounted) showToast(context, '✅ 已保存识别的账单'); } on ApiException catch (e) { if (context.mounted) showToast(context, e.message, error: true); } catch (_) { if (context.mounted) showToast(context, '保存失败,请重试', error: true); } } @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(ocrProvider); final notifier = ref.read(ocrProvider.notifier); return ListView( padding: const EdgeInsets.all(16), children: [ // 图片预览 Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), ), padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('图片预览', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF5D4037))), const SizedBox(height: 10), GestureDetector( onTap: () => _showSourceSheet(context, ref), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: state.imagePath != null ? SizedBox( height: 220, width: double.infinity, child: Image.file( File(state.imagePath!), fit: BoxFit.contain, ), ) : Container( height: 140, width: double.infinity, color: const Color(0xFFF5F5F5), child: const Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('📷', style: TextStyle(fontSize: 36)), SizedBox(height: 6), Text('点击选择图片', style: TextStyle(color: Color(0xFF8D6E63))), ], ), ), ), ), if (state.imagePath != null) Align( alignment: Alignment.centerRight, child: TextButton( onPressed: notifier.clearImage, child: const Text('清除图片', style: TextStyle(color: Color(0xFF8D6E63))), ), ), ], ), ), const SizedBox(height: 14), // 识别按钮 if (state.imagePath != null && state.results.isEmpty) SizedBox( width: double.infinity, height: 48, child: FilledButton( style: FilledButton.styleFrom( backgroundColor: const Color(0xFFC8956E), foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), ), onPressed: state.recognizing ? null : () => _recognize(context, ref), child: state.recognizing ? const SizedBox( width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2.5, color: Colors.white), ) : const Text('识别账单', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), ), ), // 识别结果 if (state.results.isNotEmpty) ...[ SizedBox( width: double.infinity, height: 44, child: FilledButton.icon( style: FilledButton.styleFrom( backgroundColor: const Color(0xFF667eea), foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(22)), ), onPressed: state.saving ? null : () => _saveDirectly(context, ref), icon: state.saving ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : const Icon(Icons.save_outlined, size: 18), label: const Text('直接存储', style: TextStyle(fontWeight: FontWeight.bold)), ), ), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.9), borderRadius: BorderRadius.circular(12), ), child: Text( '识别到 ${state.results.length} 条账单记录', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF5D4037)), ), ), const SizedBox(height: 10), for (var i = 0; i < state.results.length; i++) Padding( padding: const EdgeInsets.only(bottom: 10), child: OcrResultCard( index: i, expanded: state.expandedIndexes.contains(i), onToggle: () => notifier.toggleExpanded(i), ), ), ], const SizedBox(height: 90), // 底部导航栏避让 ], ); } void _showSourceSheet(BuildContext context, WidgetRef ref) { showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (sheetCtx) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Icons.camera_alt_outlined), title: const Text('拍照'), onTap: () { Navigator.pop(sheetCtx); _chooseImage(context, ref, ImageSource.camera); }, ), ListTile( leading: const Icon(Icons.photo_library_outlined), title: const Text('从相册选择'), onTap: () { Navigator.pop(sheetCtx); _chooseImage(context, ref, ImageSource.gallery); }, ), ], ), ), ); } }