tasks_page.dart 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:go_router/go_router.dart';
  4. import 'package:asr_client/pages/tasks/widgets/section_header.dart';
  5. import 'package:asr_client/pages/tasks/widgets/task_card.dart';
  6. import 'package:asr_client/providers/task_providers.dart';
  7. import 'package:asr_client/theme/app_colors.dart';
  8. class TasksPage extends ConsumerWidget {
  9. const TasksPage({super.key});
  10. @override
  11. Widget build(BuildContext context, WidgetRef ref) {
  12. final sections = ref.watch(taskSectionsProvider);
  13. return Scaffold(
  14. backgroundColor: AppColors.bg,
  15. appBar: AppBar(
  16. backgroundColor: AppColors.card,
  17. foregroundColor: AppColors.ink,
  18. elevation: 0,
  19. title: const Text(
  20. '今日任务',
  21. style: TextStyle(fontSize: 17, fontWeight: FontWeight.w700),
  22. ),
  23. centerTitle: true,
  24. actions: [
  25. IconButton(
  26. icon: const Icon(Icons.search, color: AppColors.ink2),
  27. onPressed: () {},
  28. ),
  29. ],
  30. ),
  31. body: ListView.builder(
  32. padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
  33. itemCount: sections.length * 2,
  34. itemBuilder: (context, index) {
  35. final sectionIndex = index ~/ 2;
  36. final section = sections[sectionIndex];
  37. if (index.isEven) {
  38. return SectionHeader(title: section.title, action: section.action);
  39. }
  40. return Column(
  41. children: section.tasks.asMap().entries.map((entry) {
  42. final cardIndex = index + entry.key;
  43. return _SlideFadeIn(
  44. key: ValueKey(entry.value.id),
  45. index: cardIndex,
  46. child: TaskCard(task: entry.value),
  47. );
  48. }).toList(),
  49. );
  50. },
  51. ),
  52. floatingActionButton: FloatingActionButton.extended(
  53. onPressed: () => context.go('/recording'),
  54. backgroundColor: AppColors.app,
  55. icon: const Icon(Icons.mic, color: Colors.white),
  56. label: const Text(
  57. '新建检测',
  58. style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700),
  59. ),
  60. ),
  61. );
  62. }
  63. }
  64. class _SlideFadeIn extends StatefulWidget {
  65. const _SlideFadeIn({super.key, required this.child, required this.index});
  66. final Widget child;
  67. final int index;
  68. @override
  69. State<_SlideFadeIn> createState() => _SlideFadeInState();
  70. }
  71. class _SlideFadeInState extends State<_SlideFadeIn>
  72. with SingleTickerProviderStateMixin {
  73. late final AnimationController _controller;
  74. late final Animation<double> _opacity;
  75. late final Animation<Offset> _offset;
  76. @override
  77. void initState() {
  78. super.initState();
  79. _controller = AnimationController(
  80. duration: const Duration(milliseconds: 350),
  81. vsync: this,
  82. );
  83. final delay = Duration(milliseconds: (widget.index * 40).clamp(0, 400));
  84. _opacity = Tween<double>(
  85. begin: 0,
  86. end: 1,
  87. ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
  88. _offset = Tween<Offset>(
  89. begin: const Offset(0, 0.15),
  90. end: Offset.zero,
  91. ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic));
  92. Future.delayed(delay, () {
  93. if (mounted) _controller.forward();
  94. });
  95. }
  96. @override
  97. void dispose() {
  98. _controller.dispose();
  99. super.dispose();
  100. }
  101. @override
  102. Widget build(BuildContext context) {
  103. return FadeTransition(
  104. opacity: _opacity,
  105. child: SlideTransition(position: _offset, child: widget.child),
  106. );
  107. }
  108. }