1
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
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<AnnotateTasksScreen> createState() => _AnnotateTasksScreenState();
|
||||
}
|
||||
|
||||
class _AnnotateTasksScreenState extends State<AnnotateTasksScreen> {
|
||||
late Future<(List<AnnotateTaskInfo>, AnnotateStats)> _future;
|
||||
bool _claiming = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_future = widget.api.tasks();
|
||||
}
|
||||
|
||||
void _reload() {
|
||||
setState(() => _future = widget.api.tasks());
|
||||
}
|
||||
|
||||
Future<void> _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<bool>(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<AnnotateTaskInfo>, 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),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user