import 'package:flutter/material.dart'; import 'package:miaoji_zhang/shared/api/api_client.dart'; import 'package:miaoji_zhang/shared/services/recognition_import_service.dart'; import 'package:miaoji_zhang/shared/services/screenshot_channel.dart'; import 'package:miaoji_zhang/shared/services/shanghai_time.dart'; import 'package:miaoji_zhang/shared/theme/app_theme.dart'; import 'package:miaoji_zhang/shared/widgets/app_controls.dart'; class RecognitionBatchPage extends StatefulWidget { final String? initialBatchId; const RecognitionBatchPage({super.key, this.initialBatchId}); @override State createState() => _RecognitionBatchPageState(); } class _RecognitionBatchPageState extends State { List _batches = const []; bool _loading = true; String? _error; String? _restoringId; String? _confirmingId; @override void initState() { super.initState(); _load(); } Future _load() async { try { final batches = await ScreenshotChannel.listRecognitionBatches(); if (!mounted) return; setState(() { _batches = batches; _loading = false; _error = null; }); } catch (error) { if (!mounted) return; setState(() { _loading = false; _error = apiErrorMessage(error); }); } } Future _restore(RecognitionBatchItem item) async { setState(() => _restoringId = item.candidateId); try { final restored = await ScreenshotChannel.restoreDroppedRecognition( item.candidateId, ); if (!restored) throw StateError('这条候选已恢复或已过期'); await RecognitionImportService.importAutomatic(); await _load(); if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('候选已恢复并入账'))); } } catch (error) { if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(apiErrorMessage(error)))); } } finally { if (mounted) setState(() => _restoringId = null); } } Future _confirm(RecognitionBatchItem item) async { setState(() => _confirmingId = item.candidateId); try { await RecognitionImportService.handleAction(context, { 'action': 'recognition_confirm', 'candidateId': item.candidateId, }); await _load(); } finally { if (mounted) setState(() => _confirmingId = null); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('最近 AI 对账')), body: RefreshIndicator(onRefresh: _load, child: _body()), ); } Widget _body() { if (_loading) { return const Center(child: CircularProgressIndicator()); } if (_error != null) { return ListView( children: [ SizedBox(height: MediaQuery.sizeOf(context).height * 0.28), Center( child: Text(_error!, style: TextStyle(color: context.jz.text2)), ), const SizedBox(height: 12), Center( child: JzActionButton( label: '重试', secondary: true, onPressed: _load, ), ), ], ); } if (_batches.isEmpty) { return ListView( children: [ SizedBox(height: MediaQuery.sizeOf(context).height * 0.3), Icon(Icons.fact_check_outlined, size: 42, color: context.jz.text3), const SizedBox(height: 12), Center( child: Text( '暂无 AI 对账记录', style: TextStyle(color: context.jz.text3), ), ), ], ); } return ListView.separated( padding: const EdgeInsets.fromLTRB(16, 8, 16, 28), itemCount: _batches.length, separatorBuilder: (_, _) => const SizedBox(height: 10), itemBuilder: (_, index) => _batchCard(_batches[index]), ); } Widget _batchCard(RecognitionBatch batch) { final count = batch.kept + batch.updated + batch.created + batch.dropped; return Card( clipBehavior: Clip.antiAlias, child: ExpansionTile( initiallyExpanded: batch.id == widget.initialBatchId, leading: Icon( batch.fallback ? Icons.offline_bolt_outlined : Icons.auto_fix_high_rounded, color: batch.fallback ? AppTheme.orange : AppTheme.primary, ), title: Text( '${ShanghaiTime.formatDateTime(batch.completedAt)} · $count 条候选', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700), ), subtitle: Padding( padding: const EdgeInsets.only(top: 4), child: Text( batch.fallback ? 'AI 不可用,已按本地识别结果处理' : '保留 ${batch.kept} · 修正 ${batch.updated} · 补全 ${batch.created} · 剔除 ${batch.dropped}', style: TextStyle(fontSize: 11.5, color: context.jz.text2), ), ), children: [ Divider(height: 1, color: context.jz.line), for (var index = 0; index < batch.items.length; index++) ...[ _batchItem(batch.items[index]), if (index != batch.items.length - 1) Divider(height: 1, indent: 54, color: context.jz.line), ], if (batch.items.isEmpty) Padding( padding: const EdgeInsets.all(16), child: Text( '本批次没有候选明细', style: TextStyle(color: context.jz.text3), ), ), ], ), ); } Widget _batchItem(RecognitionBatchItem item) { final action = _actionDisplay(item.action); final restoring = _restoringId == item.candidateId; final confirming = _confirmingId == item.candidateId; return Padding( padding: const EdgeInsets.fromLTRB(16, 13, 12, 13), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 30, height: 30, child: Icon(action.icon, size: 19, color: action.color), ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( item.merchant?.trim().isNotEmpty == true ? item.merchant!.trim() : '智能识别账单', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w700, ), ), ), const SizedBox(width: 8), Text( '${item.type == 'income' ? '+' : '-'}¥${item.amount.toStringAsFixed(2)}', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: item.type == 'income' ? AppTheme.primary : AppTheme.red, ), ), ], ), const SizedBox(height: 4), Text( '${action.label}${item.reason.isEmpty ? '' : ' · ${item.reason}'}', style: TextStyle( fontSize: 11.5, height: 1.45, color: context.jz.text2, ), ), ], ), ), if (item.canRestore) ...[ const SizedBox(width: 8), IconButton( tooltip: '恢复并入账', onPressed: restoring ? null : () => _restore(item), icon: restoring ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.restore_rounded), ), ] else if (item.state == 'pending_confirm') ...[ const SizedBox(width: 8), IconButton( tooltip: '确认入账', onPressed: confirming ? null : () => _confirm(item), icon: confirming ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.check_rounded), ), ], ], ), ); } _ActionDisplay _actionDisplay(String action) => switch (action) { 'update' => const _ActionDisplay( 'AI 已修正', Icons.edit_note_rounded, AppTheme.orange, ), 'create' => const _ActionDisplay( 'AI 已补全', Icons.add_circle_outline_rounded, AppTheme.primary, ), 'drop' => const _ActionDisplay( 'AI 已剔除', Icons.remove_circle_outline_rounded, AppTheme.red, ), 'restored' => const _ActionDisplay( '已手动恢复', Icons.restore_rounded, AppTheme.primary, ), 'fallback' => const _ActionDisplay( '本地结果', Icons.offline_bolt_outlined, AppTheme.orange, ), _ => const _ActionDisplay( 'AI 已保留', Icons.check_circle_outline_rounded, AppTheme.primary, ), }; } class _ActionDisplay { final String label; final IconData icon; final Color color; const _ActionDisplay(this.label, this.icon, this.color); }