import 'dart:io'; import 'package:flutter/material.dart'; import '../services/feedback_store.dart'; enum _FeedbackFilter { all, accurate, inaccurate } /// Browse saved accurate / inaccurate scan feedback. class FeedbackListScreen extends StatefulWidget { const FeedbackListScreen({super.key}); @override State createState() => _FeedbackListScreenState(); } class _FeedbackListScreenState extends State { List? _items; bool _loading = true; _FeedbackFilter _filter = _FeedbackFilter.all; @override void initState() { super.initState(); _reload(); } Future _reload() async { setState(() => _loading = true); final items = await FeedbackStore.instance.list(); if (!mounted) return; setState(() { _items = items; _loading = false; }); } Future _deleteItem(String id) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('删除反馈'), content: const Text('确定要删除这条反馈记录吗?'), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('取消'), ), TextButton( onPressed: () => Navigator.of(ctx).pop(true), child: const Text('删除', style: TextStyle(color: Colors.red)), ), ], ), ); if (confirmed != true) return; await FeedbackStore.instance.delete(id); await _reload(); } Future _confirmClearAll(BuildContext context) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('清空全部反馈'), content: const Text('确定要删除所有反馈记录吗?此操作不可撤销。'), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('取消'), ), TextButton( onPressed: () => Navigator.of(ctx).pop(true), child: const Text('清空全部', style: TextStyle(color: Colors.red)), ), ], ), ); if (confirmed != true) return; setState(() => _loading = true); await FeedbackStore.instance.clearAll(); await _reload(); } List get _filtered { final items = _items ?? const []; switch (_filter) { case _FeedbackFilter.all: return items; case _FeedbackFilter.accurate: return items.where((e) => e.accurate).toList(); case _FeedbackFilter.inaccurate: return items.where((e) => !e.accurate).toList(); } } @override Widget build(BuildContext context) { final items = _items; final total = items?.length ?? 0; final accurateCount = items?.where((e) => e.accurate).length ?? 0; final inaccurateCount = total - accurateCount; final rate = total == 0 ? null : accurateCount / total; return Scaffold( appBar: AppBar( title: const Text('反馈列表'), backgroundColor: Colors.transparent, actions: [ if (total > 0) IconButton( icon: const Icon(Icons.delete_sweep_outlined), tooltip: '清空全部', onPressed: () => _confirmClearAll(context), ), ], ), body: _loading ? const Center(child: CircularProgressIndicator()) : items == null || items.isEmpty ? const Center( child: Text( '暂无反馈\n在结果页点「准确」或「不准确」后会出现在这里', textAlign: TextAlign.center, style: TextStyle(color: Colors.white54), ), ) : Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _AccuracyHeader( total: total, accurateCount: accurateCount, inaccurateCount: inaccurateCount, rate: rate!, ), Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: Wrap( spacing: 8, children: [ _FilterChip( label: '全部 ($total)', selected: _filter == _FeedbackFilter.all, onTap: () => setState( () => _filter = _FeedbackFilter.all, ), ), _FilterChip( label: '准确 ($accurateCount)', selected: _filter == _FeedbackFilter.accurate, selectedColor: const Color(0xFF22C55E), onTap: () => setState( () => _filter = _FeedbackFilter.accurate, ), ), _FilterChip( label: '不准确 ($inaccurateCount)', selected: _filter == _FeedbackFilter.inaccurate, selectedColor: const Color(0xFFEF4444), onTap: () => setState( () => _filter = _FeedbackFilter.inaccurate, ), ), ], ), ), Expanded( child: RefreshIndicator( onRefresh: _reload, child: _filtered.isEmpty ? ListView( physics: const AlwaysScrollableScrollPhysics(), children: const [ SizedBox(height: 120), Center( child: Text( '当前筛选下暂无记录', style: TextStyle(color: Colors.white54), ), ), ], ) : ListView.separated( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.fromLTRB(16, 0, 16, 24), itemCount: _filtered.length, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (context, index) { final entry = _filtered[index]; return _FeedbackTile( entry: entry, onDelete: () => _deleteItem(entry.id), ); }, ), ), ), ], ), ); } } class _AccuracyHeader extends StatelessWidget { const _AccuracyHeader({ required this.total, required this.accurateCount, required this.inaccurateCount, required this.rate, }); final int total; final int accurateCount; final int inaccurateCount; final double rate; @override Widget build(BuildContext context) { final theme = Theme.of(context).textTheme; final pct = (rate * 100).toStringAsFixed(rate == 1 || rate == 0 ? 0 : 1); return Container( margin: const EdgeInsets.fromLTRB(16, 4, 16, 12), padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: Colors.white10, borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '准确率', style: theme.labelMedium?.copyWith(color: Colors.white54), ), const SizedBox(height: 2), Text( '$pct%', style: theme.headlineSmall?.copyWith( fontWeight: FontWeight.w700, color: const Color(0xFF22C55E), ), ), ], ), ), _Stat(label: '总计', value: '$total'), const SizedBox(width: 16), _Stat(label: '准确', value: '$accurateCount', color: const Color(0xFF22C55E)), const SizedBox(width: 16), _Stat( label: '不准确', value: '$inaccurateCount', color: const Color(0xFFEF4444), ), ], ), ); } } class _Stat extends StatelessWidget { const _Stat({ required this.label, required this.value, this.color, }); final String label; final String value; final Color? color; @override Widget build(BuildContext context) { final theme = Theme.of(context).textTheme; return Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( label, style: theme.labelSmall?.copyWith(color: Colors.white54), ), Text( value, style: theme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: color ?? Colors.white, ), ), ], ); } } class _FilterChip extends StatelessWidget { const _FilterChip({ required this.label, required this.selected, required this.onTap, this.selectedColor, }); final String label; final bool selected; final VoidCallback onTap; final Color? selectedColor; @override Widget build(BuildContext context) { final color = selectedColor ?? Theme.of(context).colorScheme.primary; return FilterChip( label: Text(label), selected: selected, onSelected: (_) => onTap(), selectedColor: color.withOpacity(0.25), checkmarkColor: color, labelStyle: TextStyle( color: selected ? color : Colors.white70, fontWeight: selected ? FontWeight.w600 : FontWeight.w400, ), side: BorderSide(color: selected ? color : Colors.white24), backgroundColor: Colors.white10, showCheckmark: false, ); } } class _FeedbackTile extends StatelessWidget { const _FeedbackTile({required this.entry, this.onDelete}); final FeedbackEntry entry; final VoidCallback? onDelete; @override Widget build(BuildContext context) { final theme = Theme.of(context).textTheme; final accurate = entry.accurate; final badgeColor = accurate ? const Color(0xFF22C55E) : const Color(0xFFEF4444); final badgeLabel = accurate ? '准确' : '不准确'; final score = entry.matchScore; final status = entry.matchConfident == true ? 'confident' : (entry.matchCardId == null ? '—' : 'low'); return Container( decoration: BoxDecoration( color: Colors.white10, borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: badgeColor.withOpacity(0.2), borderRadius: BorderRadius.circular(6), border: Border.all(color: badgeColor), ), child: Text( badgeLabel, style: theme.labelMedium?.copyWith( color: badgeColor, fontWeight: FontWeight.w600, ), ), ), const Spacer(), if (onDelete != null) InkWell( borderRadius: BorderRadius.circular(6), onTap: onDelete, child: const Padding( padding: EdgeInsets.all(4), child: Icon(Icons.delete_outline, size: 18, color: Colors.white38), ), ), Text( _formatTime(entry.createdAt), style: theme.labelSmall?.copyWith(color: Colors.white54), ), ], ), const SizedBox(height: 10), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: _LabeledThumb( label: '拍照图', child: _FileImage(path: entry.photoPath), ), ), const SizedBox(width: 8), Expanded( child: _LabeledThumb( label: '裁剪图', child: _FileImage(path: entry.cropPath), ), ), const SizedBox(width: 8), Expanded( child: _LabeledThumb( label: '最佳匹配', child: entry.matchAssetPath == null ? const ColoredBox( color: Colors.black26, child: Center( child: Icon(Icons.image_not_supported_outlined), ), ) : Image.asset( entry.matchAssetPath!, fit: BoxFit.cover, errorBuilder: (_, __, ___) => const ColoredBox( color: Colors.black26, child: Center( child: Icon(Icons.broken_image_outlined), ), ), ), ), ), ], ), const SizedBox(height: 8), Text( entry.matchCardId == null ? '无匹配结果' : '${entry.matchCardId} · ${score?.toStringAsFixed(2) ?? '—'} ($status)', maxLines: 2, overflow: TextOverflow.ellipsis, style: theme.bodySmall?.copyWith(color: Colors.white70), ), ], ), ); } static String _formatTime(DateTime t) { final local = t.toLocal(); String two(int n) => n.toString().padLeft(2, '0'); return '${local.month}/${local.day} ${two(local.hour)}:${two(local.minute)}'; } } class _LabeledThumb extends StatelessWidget { const _LabeledThumb({required this.label, required this.child}); final String label; final Widget child; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( label, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Colors.white54, ), ), const SizedBox(height: 4), AspectRatio( aspectRatio: 0.72, child: ClipRRect( borderRadius: BorderRadius.circular(8), child: child, ), ), ], ); } } class _FileImage extends StatelessWidget { const _FileImage({required this.path}); final String path; @override Widget build(BuildContext context) { final file = File(path); if (!file.existsSync()) { return const ColoredBox( color: Colors.black26, child: Center(child: Icon(Icons.broken_image_outlined)), ); } return Image.file( file, fit: BoxFit.cover, errorBuilder: (_, __, ___) => const ColoredBox( color: Colors.black26, child: Center(child: Icon(Icons.broken_image_outlined)), ), ); } }