add_challenge_screen.dart 5.9 KB

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