challenge_list_screen.dart 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. // challenge_list_screen.dart
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter_riverpod/flutter_riverpod.dart';
  4. import 'package:intl/intl.dart';
  5. import 'package:japp_flutter/core/models/challenge_model.dart';
  6. import 'package:japp_flutter/features/challenge/view_models/challenge_list_vm.dart';
  7. class ChallengeListScreen extends ConsumerWidget {
  8. const ChallengeListScreen({super.key});
  9. @override
  10. Widget build(BuildContext context, WidgetRef ref) {
  11. final challengesState = ref.watch(challengeListProvider);
  12. return Scaffold(
  13. appBar: AppBar(
  14. title: const Text('挑战列表'),
  15. actions: [
  16. IconButton(
  17. icon: const Icon(Icons.refresh),
  18. onPressed: () => ref.refresh(challengeListProvider.future),
  19. ),
  20. ],
  21. ),
  22. body: _buildBody(challengesState, ref),
  23. floatingActionButton: FloatingActionButton(
  24. onPressed: () => Navigator.pushNamed(context, '/challenge/add'),
  25. child: const Icon(Icons.add),
  26. ),
  27. );
  28. }
  29. Widget _buildBody(AsyncValue<List<ChallengeModel>> state, WidgetRef ref) {
  30. return state.when(
  31. loading: () => const Center(child: CircularProgressIndicator()),
  32. error: (error, stack) => Center(
  33. child: Column(
  34. mainAxisAlignment: MainAxisAlignment.center,
  35. children: [
  36. const Icon(Icons.error, color: Colors.red, size: 48),
  37. const SizedBox(height: 16),
  38. Text('加载失败: $error', style: const TextStyle(color: Colors.red)),
  39. const SizedBox(height: 16),
  40. ElevatedButton(
  41. onPressed: () => ref.refresh(challengeListProvider.future),
  42. child: const Text('重试'),
  43. ),
  44. ],
  45. ),
  46. ),
  47. data: (challenges) => challenges.isEmpty
  48. ? const Center(child: Text('暂无挑战', style: TextStyle(fontSize: 18)))
  49. : RefreshIndicator(
  50. onRefresh: () => ref.read(challengeListProvider.notifier).refresh(),
  51. child: ListView.builder(
  52. padding: const EdgeInsets.all(16),
  53. itemCount: challenges.length,
  54. itemBuilder: (_, index) => ChallengeCard(challenge: challenges[index]),
  55. ),
  56. ),
  57. );
  58. }
  59. }
  60. class ChallengeCard extends ConsumerWidget {
  61. final ChallengeModel challenge;
  62. const ChallengeCard({super.key, required this.challenge});
  63. @override
  64. Widget build(BuildContext context, WidgetRef ref) {
  65. final theme = Theme.of(context);
  66. return Card(
  67. elevation: 2,
  68. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
  69. margin: const EdgeInsets.only(bottom: 16),
  70. child: Padding(
  71. padding: const EdgeInsets.all(16),
  72. child: Column(
  73. crossAxisAlignment: CrossAxisAlignment.start,
  74. children: [
  75. Row(
  76. children: [
  77. Expanded(
  78. child: Text(
  79. challenge.title,
  80. style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
  81. ),
  82. ),
  83. _DifficultyChip(difficulty: challenge.difficulty),
  84. ],
  85. ),
  86. const SizedBox(height: 8),
  87. Text(
  88. challenge.description,
  89. style: TextStyle(color: Colors.grey[700], fontSize: 14),
  90. ),
  91. const SizedBox(height: 16),
  92. _ProgressBar(
  93. startDate: challenge.startDate,
  94. endDate: challenge.endDate,
  95. remainingDays: challenge.remainingDays!,
  96. ),
  97. const SizedBox(height: 12),
  98. _CardFooter(
  99. participants: challenge.participants,
  100. remainingDays: challenge.remainingDays!,
  101. ),
  102. ],
  103. ),
  104. ),
  105. );
  106. }
  107. }
  108. // 将复杂子组件拆分为独立组件
  109. class _DifficultyChip extends StatelessWidget {
  110. final String difficulty;
  111. const _DifficultyChip({required this.difficulty});
  112. Color _getColor() {
  113. switch (difficulty) {
  114. case '简单': return Colors.green;
  115. case '中等': return Colors.orange;
  116. case '困难': return Colors.red;
  117. default: return Colors.grey;
  118. }
  119. }
  120. @override
  121. Widget build(BuildContext context) {
  122. return Chip(
  123. label: Text(difficulty, style: const TextStyle(color: Colors.white)),
  124. backgroundColor: _getColor(),
  125. );
  126. }
  127. }
  128. class _ProgressBar extends StatelessWidget {
  129. final DateTime startDate;
  130. final DateTime endDate;
  131. final int remainingDays;
  132. const _ProgressBar({
  133. required this.startDate,
  134. required this.endDate,
  135. required this.remainingDays,
  136. });
  137. @override
  138. Widget build(BuildContext context) {
  139. final theme = Theme.of(context);
  140. final totalDays = endDate.difference(startDate).inDays;
  141. final passedDays = totalDays - remainingDays;
  142. final progress = passedDays / totalDays;
  143. return Column(
  144. crossAxisAlignment: CrossAxisAlignment.start,
  145. children: [
  146. Text('${(progress * 100).toStringAsFixed(0)}%', style: const TextStyle(fontSize: 12)),
  147. const SizedBox(height: 4),
  148. LinearProgressIndicator(
  149. value: progress,
  150. backgroundColor: Colors.grey[200],
  151. borderRadius: BorderRadius.circular(10),
  152. minHeight: 8,
  153. color: theme.primaryColor,
  154. ),
  155. const SizedBox(height: 4),
  156. Row(
  157. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  158. children: [
  159. Text(DateFormat('MM/dd').format(startDate), style: const TextStyle(fontSize: 12)),
  160. Text(DateFormat('MM/dd').format(endDate), style: const TextStyle(fontSize: 12)),
  161. ],
  162. ),
  163. ],
  164. );
  165. }
  166. }
  167. class _CardFooter extends StatelessWidget {
  168. final int participants;
  169. final int remainingDays;
  170. const _CardFooter({
  171. required this.participants,
  172. required this.remainingDays,
  173. });
  174. @override
  175. Widget build(BuildContext context) {
  176. return Row(
  177. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  178. children: [
  179. _InfoChip(icon: Icons.people, text: '$participants人参加'),
  180. _InfoChip(icon: Icons.calendar_today, text: '剩余${remainingDays}天'),
  181. ],
  182. );
  183. }
  184. }
  185. class _InfoChip extends StatelessWidget {
  186. final IconData icon;
  187. final String text;
  188. const _InfoChip({
  189. required this.icon,
  190. required this.text,
  191. });
  192. @override
  193. Widget build(BuildContext context) {
  194. return Container(
  195. padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
  196. decoration: BoxDecoration(
  197. color: Colors.grey[100],
  198. borderRadius: BorderRadius.circular(20),
  199. ),
  200. child: Row(
  201. children: [
  202. Icon(icon, size: 16, color: Colors.grey[600]),
  203. const SizedBox(width: 4),
  204. Text(text, style: TextStyle(color: Colors.grey[700])),
  205. ],
  206. ),
  207. );
  208. }
  209. }