198 lines
7.1 KiB
Dart
198 lines
7.1 KiB
Dart
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),
|
||
),
|
||
),
|
||
]),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|