add_challenge_screen.dart 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. // features/challenge/add/add_challenge_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_add_vm.dart';
  7. class AddChallengeScreen extends ConsumerWidget {
  8. const AddChallengeScreen({super.key});
  9. @override
  10. Widget build(BuildContext context, WidgetRef ref) {
  11. final challengeState = ref.watch(addChallengeProvider);
  12. final theme = Theme.of(context);
  13. return Scaffold(
  14. appBar: AppBar(
  15. title: const Text('创建新挑战'),
  16. actions: [
  17. IconButton(
  18. icon: const Icon(Icons.check),
  19. onPressed: () async {
  20. await ref.read(addChallengeProvider.notifier).submitChallenge();
  21. if (context.mounted) Navigator.pop(context);
  22. },
  23. tooltip: '提交',
  24. ),
  25. ],
  26. ),
  27. body: challengeState.when(
  28. loading: () => const Center(child: CircularProgressIndicator()),
  29. error: (error, _) => Center(child: Text('错误: $error')),
  30. data: (challenge) => _BuildForm(challenge: challenge),
  31. ),
  32. );
  33. }
  34. }
  35. class _BuildForm extends ConsumerWidget {
  36. final ChallengeModel challenge;
  37. const _BuildForm({required this.challenge});
  38. @override
  39. Widget build(BuildContext context, WidgetRef ref) {
  40. return SingleChildScrollView(
  41. padding: const EdgeInsets.all(16),
  42. child: Column(
  43. crossAxisAlignment: CrossAxisAlignment.start,
  44. children: [
  45. _TitleField(initialValue: challenge.title),
  46. const SizedBox(height: 24),
  47. _DateRangeField(
  48. startDate: challenge.startDate,
  49. endDate: challenge.endDate,
  50. ),
  51. const SizedBox(height: 24),
  52. _DifficultySelector(currentDifficulty: challenge.difficulty),
  53. const SizedBox(height: 24),
  54. _DescriptionField(initialValue: challenge.description),
  55. ],
  56. ),
  57. );
  58. }
  59. }
  60. class _TitleField extends ConsumerWidget {
  61. final String initialValue;
  62. const _TitleField({required this.initialValue});
  63. @override
  64. Widget build(BuildContext context, WidgetRef ref) {
  65. return TextFormField(
  66. initialValue: initialValue,
  67. decoration: const InputDecoration(
  68. labelText: '挑战标题*',
  69. border: OutlineInputBorder(),
  70. ),
  71. onChanged: (value) => ref.read(addChallengeProvider.notifier).updateTitle(value),
  72. );
  73. }
  74. }
  75. class _DescriptionField extends ConsumerWidget {
  76. final String initialValue;
  77. const _DescriptionField({required this.initialValue});
  78. @override
  79. Widget build(BuildContext context, WidgetRef ref) {
  80. return TextFormField(
  81. initialValue: initialValue,
  82. decoration: const InputDecoration(
  83. labelText: '挑战描述',
  84. border: OutlineInputBorder(),
  85. alignLabelWithHint: true,
  86. ),
  87. maxLines: 5,
  88. onChanged: (value) => ref.read(addChallengeProvider.notifier).updateDescription(value),
  89. );
  90. }
  91. }
  92. class _DifficultySelector extends ConsumerWidget {
  93. final String currentDifficulty;
  94. static const difficulties = ['简单', '中等', '困难'];
  95. const _DifficultySelector({required this.currentDifficulty});
  96. @override
  97. Widget build(BuildContext context, WidgetRef ref) {
  98. return Column(
  99. crossAxisAlignment: CrossAxisAlignment.start,
  100. children: [
  101. const Text('难度级别*', style: TextStyle(fontSize: 16)),
  102. const SizedBox(height: 8),
  103. Wrap(
  104. spacing: 8,
  105. children: difficulties.map((level) {
  106. return ChoiceChip(
  107. label: Text(level),
  108. selected: currentDifficulty == level,
  109. onSelected: (selected) {
  110. if (selected) {
  111. ref.read(addChallengeProvider.notifier).updateDifficulty(level);
  112. }
  113. },
  114. );
  115. }).toList(),
  116. ),
  117. ],
  118. );
  119. }
  120. }
  121. class _DateRangeField extends ConsumerWidget {
  122. final DateTime startDate;
  123. final DateTime endDate;
  124. const _DateRangeField({
  125. required this.startDate,
  126. required this.endDate,
  127. });
  128. Future<void> _selectDate(BuildContext context, bool isStartDate, WidgetRef ref) async {
  129. final initialDate = isStartDate ? startDate : endDate;
  130. final picked = await showDatePicker(
  131. context: context,
  132. initialDate: initialDate,
  133. firstDate: DateTime.now(),
  134. lastDate: DateTime(2100),
  135. );
  136. if (picked != null) {
  137. final newStart = isStartDate ? picked : startDate;
  138. final newEnd = isStartDate ? endDate : picked;
  139. ref.read(addChallengeProvider.notifier).updateDateRange(newStart, newEnd);
  140. }
  141. }
  142. @override
  143. Widget build(BuildContext context, WidgetRef ref) {
  144. return Column(
  145. crossAxisAlignment: CrossAxisAlignment.start,
  146. children: [
  147. const Text('挑战周期*', style: TextStyle(fontSize: 16)),
  148. const SizedBox(height: 8),
  149. Row(
  150. children: [
  151. Expanded(
  152. child: OutlinedButton(
  153. onPressed: () => _selectDate(context, true, ref),
  154. child: Text(DateFormat('yyyy/MM/dd').format(startDate)),
  155. ),
  156. ),
  157. const Padding(
  158. padding: EdgeInsets.symmetric(horizontal: 8),
  159. child: Text('至'),
  160. ),
  161. Expanded(
  162. child: OutlinedButton(
  163. onPressed: () => _selectDate(context, false, ref),
  164. child: Text(DateFormat('yyyy/MM/dd').format(endDate)),
  165. ),
  166. ),
  167. ],
  168. ),
  169. const SizedBox(height: 4),
  170. Text(
  171. '总天数: ${endDate.difference(startDate).inDays}天',
  172. style: const TextStyle(color: Colors.grey),
  173. ),
  174. ],
  175. );
  176. }
  177. }