challenge_list_screen.dart 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  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.read(challengeListProvider.notifier).refresh(),
  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: () =>
  42. ref.read(challengeListProvider.notifier).refresh(),
  43. child: const Text('重试'),
  44. ),
  45. ],
  46. ),
  47. ),
  48. data: (challenges) => challenges.isEmpty
  49. ? const Center(child: Text('暂无挑战', style: TextStyle(fontSize: 18)))
  50. : RefreshIndicator(
  51. onRefresh: () =>
  52. ref.read(challengeListProvider.notifier).refresh(),
  53. child: ListView.builder(
  54. padding: const EdgeInsets.all(16),
  55. itemCount: challenges.length,
  56. // itemBuilder: (_, index) => ChallengeCard(challenge: challenges[index]),
  57. itemBuilder: (context, index) => ListTile(
  58. title: Text(challenges[index].title),
  59. // 其他列表项内容
  60. ),
  61. ),
  62. ),
  63. );
  64. }
  65. }
  66. class ChallengeCard extends StatelessWidget {
  67. final ChallengeModel challenge;
  68. const ChallengeCard({super.key, required this.challenge});
  69. @override
  70. Widget build(BuildContext context) {
  71. final theme = Theme.of(context);
  72. return Card(
  73. elevation: 2,
  74. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
  75. margin: const EdgeInsets.only(bottom: 16),
  76. child: Padding(
  77. padding: const EdgeInsets.all(16),
  78. child: Column(
  79. crossAxisAlignment: CrossAxisAlignment.start,
  80. children: [
  81. Row(
  82. children: [
  83. Expanded(
  84. child: Text(
  85. challenge.title,
  86. style: const TextStyle(
  87. fontSize: 18,
  88. fontWeight: FontWeight.bold,
  89. ),
  90. ),
  91. ),
  92. _DifficultyChip(difficulty: challenge.difficulty),
  93. ],
  94. ),
  95. const SizedBox(height: 8),
  96. Text(
  97. challenge.description,
  98. style: TextStyle(color: Colors.grey[700], fontSize: 14),
  99. ),
  100. const SizedBox(height: 16),
  101. _ProgressBar(
  102. startDate: challenge.startDate,
  103. endDate: challenge.endDate,
  104. remainingDays: challenge.remainingDays!,
  105. ),
  106. const SizedBox(height: 12),
  107. _CardFooter(
  108. participants: challenge.participants,
  109. remainingDays: challenge.remainingDays!,
  110. ),
  111. ],
  112. ),
  113. ),
  114. );
  115. }
  116. }
  117. // 将复杂子组件拆分为独立组件
  118. class _DifficultyChip extends StatelessWidget {
  119. final String difficulty;
  120. const _DifficultyChip({required this.difficulty});
  121. Color _getColor() {
  122. switch (difficulty) {
  123. case '简单':
  124. return Colors.green;
  125. case '中等':
  126. return Colors.orange;
  127. case '困难':
  128. return Colors.red;
  129. default:
  130. return Colors.grey;
  131. }
  132. }
  133. @override
  134. Widget build(BuildContext context) {
  135. return Chip(
  136. label: Text(difficulty, style: const TextStyle(color: Colors.white)),
  137. backgroundColor: _getColor(),
  138. );
  139. }
  140. }
  141. class _ProgressBar extends StatelessWidget {
  142. final DateTime startDate;
  143. final DateTime endDate;
  144. final int remainingDays;
  145. const _ProgressBar({
  146. required this.startDate,
  147. required this.endDate,
  148. required this.remainingDays,
  149. });
  150. @override
  151. Widget build(BuildContext context) {
  152. final theme = Theme.of(context);
  153. final totalDays = endDate.difference(startDate).inDays;
  154. final passedDays = totalDays - remainingDays;
  155. final progress = passedDays / totalDays;
  156. return Column(
  157. crossAxisAlignment: CrossAxisAlignment.start,
  158. children: [
  159. Text(
  160. '${(progress * 100).toStringAsFixed(0)}%',
  161. style: const TextStyle(fontSize: 12),
  162. ),
  163. const SizedBox(height: 4),
  164. LinearProgressIndicator(
  165. value: progress,
  166. backgroundColor: Colors.grey[200],
  167. borderRadius: BorderRadius.circular(10),
  168. minHeight: 8,
  169. color: theme.primaryColor,
  170. ),
  171. const SizedBox(height: 4),
  172. Row(
  173. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  174. children: [
  175. Text(
  176. DateFormat('MM/dd').format(startDate),
  177. style: const TextStyle(fontSize: 12),
  178. ),
  179. Text(
  180. DateFormat('MM/dd').format(endDate),
  181. style: const TextStyle(fontSize: 12),
  182. ),
  183. ],
  184. ),
  185. ],
  186. );
  187. }
  188. }
  189. class _CardFooter extends StatelessWidget {
  190. final int participants;
  191. final int remainingDays;
  192. const _CardFooter({required this.participants, required this.remainingDays});
  193. @override
  194. Widget build(BuildContext context) {
  195. return Row(
  196. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  197. children: [
  198. _InfoChip(icon: Icons.people, text: '$participants人参加'),
  199. _InfoChip(icon: Icons.calendar_today, text: '剩余${remainingDays}天'),
  200. ],
  201. );
  202. }
  203. }
  204. class _InfoChip extends StatelessWidget {
  205. final IconData icon;
  206. final String text;
  207. const _InfoChip({required this.icon, required this.text});
  208. @override
  209. Widget build(BuildContext context) {
  210. return Container(
  211. padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
  212. decoration: BoxDecoration(
  213. color: Colors.grey[100],
  214. borderRadius: BorderRadius.circular(20),
  215. ),
  216. child: Row(
  217. children: [
  218. Icon(icon, size: 16, color: Colors.grey[600]),
  219. const SizedBox(width: 4),
  220. Text(text, style: TextStyle(color: Colors.grey[700])),
  221. ],
  222. ),
  223. );
  224. }
  225. }