add_challenge_screen.dart 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  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. import 'package:japp_flutter/features/widgets/app_dropdown.dart';
  9. import 'package:japp_flutter/features/widgets/date_picker.dart';
  10. class AddChallengeScreen extends ConsumerWidget {
  11. const AddChallengeScreen({super.key});
  12. @override
  13. Widget build(BuildContext context, WidgetRef ref) {
  14. final challengeState = ref.watch(addChallengeProvider);
  15. final theme = Theme.of(context);
  16. return Scaffold(
  17. appBar: AppBar(
  18. title: const Text('创建新挑战'),
  19. actions: [
  20. IconButton(
  21. icon: const Icon(Icons.check),
  22. onPressed: () async {
  23. await ref.read(addChallengeProvider.notifier).submitChallenge();
  24. if (context.mounted) Navigator.pop(context);
  25. },
  26. tooltip: '提交',
  27. ),
  28. ],
  29. ),
  30. body: challengeState.when(
  31. loading: () => const Center(child: CircularProgressIndicator()),
  32. error: (error, _) => Center(child: Text('错误: $error')),
  33. data: (challenge) => _BuildForm(challenge: challenge),
  34. ),
  35. );
  36. }
  37. }
  38. class _BuildForm extends ConsumerWidget {
  39. final ChallengeModel challenge;
  40. const _BuildForm({required this.challenge});
  41. @override
  42. Widget build(BuildContext context, WidgetRef ref) {
  43. return SingleChildScrollView(
  44. padding: const EdgeInsets.all(16),
  45. child: Column(
  46. crossAxisAlignment: CrossAxisAlignment.start,
  47. children: [
  48. TextFormField(
  49. initialValue: challenge.title,
  50. decoration: const InputDecoration(
  51. labelText: '目标',
  52. hintText: '请输入目标名称',
  53. border: OutlineInputBorder(),
  54. ),
  55. onChanged: (value) =>
  56. ref.read(addChallengeProvider.notifier).updateTitle(value),
  57. ),
  58. const SizedBox(height: 16),
  59. TextFormField(
  60. initialValue: challenge.description,
  61. decoration: const InputDecoration(
  62. labelText: '简介',
  63. hintText: '请输入目标的简介',
  64. border: OutlineInputBorder(),
  65. alignLabelWithHint: true,
  66. ),
  67. maxLines: 3,
  68. onChanged: (value) => ref
  69. .read(addChallengeProvider.notifier)
  70. .updateDescription(value),
  71. ),
  72. const SizedBox(height: 16),
  73. AppDropdown(
  74. label: '目标类型',
  75. value: challenge.actionType,
  76. items: challengeActionTypeOpts,
  77. onChanged: (value) => ref
  78. .read(addChallengeProvider.notifier)
  79. .updateActionType(value ?? 0),
  80. ),
  81. const SizedBox(height: 16),
  82. AppDropdown(
  83. label: '挑战状态',
  84. value: challenge.status,
  85. items: challengeStatusOpts,
  86. onChanged: (value) => ref
  87. .read(addChallengeProvider.notifier)
  88. .updateStatus(value ?? 0),
  89. ),
  90. const SizedBox(height: 16),
  91. AppDropdown(
  92. label: '挑战难度',
  93. value: challenge.difficulty,
  94. items: challengeDifficultyOpts,
  95. onChanged: (value) => ref
  96. .read(addChallengeProvider.notifier)
  97. .updateDifficulty(value ?? 0),
  98. ),
  99. const SizedBox(height: 16),
  100. _DateRangeField(
  101. startDate: challenge.startDate!,
  102. endDate: challenge.endDate!,
  103. ),
  104. const SizedBox(height: 16),
  105. TextFormField(
  106. keyboardType: TextInputType.number,
  107. initialValue: challenge.sort.toString(),
  108. decoration: const InputDecoration(
  109. labelText: '排序',
  110. border: OutlineInputBorder(),
  111. alignLabelWithHint: true,
  112. ),
  113. onChanged: (value) => ref
  114. .read(addChallengeProvider.notifier)
  115. .updateSort(int.parse(value)),
  116. ),
  117. const SizedBox(height: 16),
  118. DatePickerField(
  119. labelText: '计划完成日期',
  120. onDateSelected: (date) => ref
  121. .read(addChallengeProvider.notifier)
  122. .updatePlanFinishDate(date ?? DateTime.now()),
  123. ),
  124. const SizedBox(height: 16),
  125. DatePickerField(
  126. labelText: '完成日期',
  127. onDateSelected: (date) => ref
  128. .read(addChallengeProvider.notifier)
  129. .updateFinishDate(date ?? DateTime.now()),
  130. ),
  131. const SizedBox(height: 16),
  132. TextFormField(
  133. initialValue: challenge.remark,
  134. decoration: const InputDecoration(
  135. labelText: '备注',
  136. border: OutlineInputBorder(),
  137. alignLabelWithHint: true,
  138. ),
  139. maxLines: 3,
  140. onChanged: (value) =>
  141. ref.read(addChallengeProvider.notifier).updateRemark(value),
  142. ),
  143. const SizedBox(height: 16),
  144. ],
  145. ),
  146. );
  147. }
  148. }
  149. class _DateRangeField extends ConsumerWidget {
  150. final DateTime startDate;
  151. final DateTime endDate;
  152. const _DateRangeField({required this.startDate, required this.endDate});
  153. Future<void> _selectDate(
  154. BuildContext context,
  155. bool isStartDate,
  156. WidgetRef ref,
  157. ) async {
  158. final initialDate = isStartDate ? startDate : endDate;
  159. final picked = await showDatePicker(
  160. context: context,
  161. initialDate: initialDate,
  162. firstDate: DateTime.now(),
  163. lastDate: DateTime(2100),
  164. );
  165. if (picked != null) {
  166. final newStart = isStartDate ? picked : startDate;
  167. final newEnd = isStartDate ? endDate : picked;
  168. ref.read(addChallengeProvider.notifier).updateDateRange(newStart, newEnd);
  169. }
  170. }
  171. @override
  172. Widget build(BuildContext context, WidgetRef ref) {
  173. return Column(
  174. crossAxisAlignment: CrossAxisAlignment.start,
  175. children: [
  176. const Text('挑战周期*', style: TextStyle(fontSize: 16)),
  177. const SizedBox(height: 8),
  178. Row(
  179. children: [
  180. Expanded(
  181. child: OutlinedButton(
  182. onPressed: () => _selectDate(context, true, ref),
  183. child: Text(DateFormat('yyyy/MM/dd').format(startDate)),
  184. ),
  185. ),
  186. const Padding(
  187. padding: EdgeInsets.symmetric(horizontal: 8),
  188. child: Text('至'),
  189. ),
  190. Expanded(
  191. child: OutlinedButton(
  192. onPressed: () => _selectDate(context, false, ref),
  193. child: Text(DateFormat('yyyy/MM/dd').format(endDate)),
  194. ),
  195. ),
  196. ],
  197. ),
  198. const SizedBox(height: 4),
  199. Text(
  200. '总天数: ${endDate.difference(startDate).inDays}天',
  201. style: const TextStyle(color: Colors.grey),
  202. ),
  203. ],
  204. );
  205. }
  206. }