import 'package:flutter/material.dart'; import 'annotate_api.dart'; import 'annotator_screen.dart'; String _fmt(DateTime t) { String p(int n) => n.toString().padLeft(2, '0'); return '${t.month}/${t.day} ${p(t.hour)}:${p(t.minute)}'; } /// 标注赚时长首页:我的统计(进度/今日已得/冻结态)+ 可领任务列表。 /// 领取后进入标注器逐张画框提交,提交满 [AnnotateStats.rewardPerImages] 张自动到账时长。 class AnnotateTasksScreen extends StatefulWidget { final AnnotateApi api; const AnnotateTasksScreen({super.key, required this.api}); @override State createState() => _AnnotateTasksScreenState(); } class _AnnotateTasksScreenState extends State { late Future<(List, AnnotateStats)> _future; bool _claiming = false; @override void initState() { super.initState(); _future = widget.api.tasks(); } void _reload() { setState(() => _future = widget.api.tasks()); } Future _claim(AnnotateTaskInfo task) async { if (_claiming) return; setState(() => _claiming = true); try { final images = await widget.api.claim(task.id); if (!mounted) return; if (images.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('任务池暂时没有可领取的图片,稍后再来'))); _reload(); return; } final earned = await Navigator.of(context).push(MaterialPageRoute( builder: (_) => AnnotatorScreen(api: widget.api, task: task, images: images), )); if (earned == true) _reload(); // 本批有提交:回列表刷新进度/奖励 } on SessionExpiredException { if (mounted) _sessionExpired(); } catch (e) { if (mounted) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('$e'.replaceFirst('Exception: ', '')))); } } finally { if (mounted) setState(() => _claiming = false); } } void _sessionExpired() { ScaffoldMessenger.of(context) .showSnackBar(const SnackBar(content: Text('登录已失效,请重新登录'))); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('标注赚时长')), body: FutureBuilder<(List, AnnotateStats)>( future: _future, builder: (context, snap) { if (snap.connectionState != ConnectionState.done) { return const Center(child: CircularProgressIndicator()); } if (snap.hasError) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text('加载失败:${snap.error}'.replaceFirst('Exception: ', '')), const SizedBox(height: 12), FilledButton(onPressed: _reload, child: const Text('重试')), ], ), ); } final (tasks, stats) = snap.data!; return RefreshIndicator( onRefresh: () async => _reload(), child: ListView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(16), children: [ _StatsCard(stats: stats), const SizedBox(height: 16), Text('可领任务', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), if (tasks.isEmpty) const Padding( padding: EdgeInsets.symmetric(vertical: 32), child: Center(child: Text('暂无可领任务,敬请期待')), ) else ...tasks.map((t) => Card( margin: const EdgeInsets.only(bottom: 10), child: ListTile( title: Text(t.name, style: const TextStyle(fontWeight: FontWeight.w600)), subtitle: Text( '物种:${t.species.isEmpty ? t.datasetName : t.species} · 池余量 ${t.poolRemain} 张'), trailing: FilledButton( onPressed: _claiming || stats.frozen || t.poolRemain <= 0 ? null : () => _claim(t), child: const Text('领取'), ), ), )), ], ), ); }, ), ); } } class _StatsCard extends StatelessWidget { final AnnotateStats stats; const _StatsCard({required this.stats}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.emoji_events, color: Colors.amber.shade700, size: 28), const SizedBox(width: 8), Text('标 ${stats.rewardPerImages} 张得 ${stats.rewardMinutes} 分钟', style: theme.textTheme.titleMedium ?.copyWith(fontWeight: FontWeight.w600)), ], ), const SizedBox(height: 12), LinearProgressIndicator( value: stats.rewardPerImages <= 0 ? 0 : (stats.progressDone % stats.rewardPerImages) / stats.rewardPerImages, ), const SizedBox(height: 6), Text( '本档进度 ${stats.progressDone}/${stats.rewardPerImages} · 今日已得 ${stats.todayEarnedMinutes}/${stats.dailyCapMinutes} 分钟 · 累计 ${stats.totalEarnedMinutes} 分钟', style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey.shade600), ), Text( '通过 ${stats.approved} / 拒绝 ${stats.rejected}(通过率 ${(stats.approveRatio * 100).toStringAsFixed(0)}%)', style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey.shade600), ), if (stats.frozen) ...[ const SizedBox(height: 8), Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.red.shade50, borderRadius: BorderRadius.circular(8), ), child: Row(children: [ const Icon(Icons.block, color: Colors.red, size: 20), const SizedBox(width: 8), Expanded( child: Text( '标注质量未达标,资格冻结中(${_fmt(stats.frozenUntil!)} 解冻)', style: const TextStyle(color: Colors.red, fontSize: 13), ), ), ]), ), ], ], ), ), ); } }