overtime_apply_page.dart 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:go_router/go_router.dart';
  4. import 'package:tdesign_flutter/tdesign_flutter.dart';
  5. import '../../core/theme/app_colors.dart';
  6. import '../shell/nav_bar_config.dart';
  7. import '../../core/utils/date_utils.dart' as du;
  8. import '../../shared/widgets/action_bar.dart';
  9. import '../../shared/widgets/form_section.dart';
  10. import '../../shared/widgets/form_field_row.dart';
  11. import '../../core/i18n/app_localizations.dart';
  12. import 'overtime_apply_controller.dart';
  13. class OvertimeApplyPage extends ConsumerStatefulWidget {
  14. final String? editId;
  15. const OvertimeApplyPage({super.key, this.editId});
  16. @override
  17. ConsumerState<OvertimeApplyPage> createState() => _OvertimeApplyPageState();
  18. }
  19. class _OvertimeApplyPageState extends ConsumerState<OvertimeApplyPage> {
  20. final _reasonController = TextEditingController();
  21. static const _types = ['工作日加班', '休息日加班', '节假日加班'];
  22. static const _compensations = ['加班费', '调休'];
  23. @override
  24. void initState() {
  25. super.initState();
  26. final state = ref.read(overtimeApplyProvider(widget.editId));
  27. _reasonController.text = state.overtime.reason;
  28. }
  29. @override
  30. void dispose() {
  31. _reasonController.dispose();
  32. super.dispose();
  33. }
  34. @override
  35. Widget build(BuildContext context) {
  36. final ctrl = ref.watch(overtimeApplyProvider(widget.editId).notifier);
  37. final state = ref.watch(overtimeApplyProvider(widget.editId));
  38. final l10n = AppLocalizations.of(context);
  39. ref
  40. .read(navBarConfigProvider.notifier)
  41. .update(
  42. NavBarConfig(
  43. title: l10n.get('overtimeApply'),
  44. showBack: true,
  45. onBack: () => context.pop(),
  46. ),
  47. );
  48. return Column(
  49. children: [
  50. Expanded(
  51. child: SingleChildScrollView(
  52. padding: const EdgeInsets.all(16),
  53. child: FormSection(
  54. title: '加班信息',
  55. children: [
  56. FormFieldRow(
  57. label: '加班类型',
  58. value: state.overtime.otType,
  59. onTap: () =>
  60. _showPicker(_types, ctrl.updateType, title: '选择加班类型'),
  61. ),
  62. FormFieldRow(
  63. label: '补偿方式',
  64. value: state.overtime.compensationType,
  65. onTap: () => _showPicker(
  66. _compensations,
  67. ctrl.updateCompensation,
  68. title: '选择补偿方式',
  69. ),
  70. ),
  71. FormFieldRow(
  72. label: '开始时间',
  73. value: du.DateUtils.formatDateTime(state.overtime.startTime),
  74. onTap: () => _pickDateTime(
  75. ctrl.updateStartTime,
  76. state.overtime.startTime,
  77. ),
  78. ),
  79. FormFieldRow(
  80. label: '结束时间',
  81. value: du.DateUtils.formatDateTime(state.overtime.endTime),
  82. onTap: () =>
  83. _pickDateTime(ctrl.updateEndTime, state.overtime.endTime),
  84. ),
  85. const SizedBox(height: 12),
  86. const Text(
  87. '净加班时长',
  88. style: TextStyle(
  89. fontSize: AppFontSizes.body,
  90. color: AppColors.textSecondary,
  91. ),
  92. ),
  93. const SizedBox(height: 8),
  94. Container(
  95. height: 80,
  96. width: double.infinity,
  97. decoration: BoxDecoration(
  98. color: AppColors.primaryLight,
  99. borderRadius: BorderRadius.circular(8),
  100. ),
  101. child: Center(
  102. child: Text(
  103. '${state.overtime.otHours.toStringAsFixed(1)} 小时',
  104. style: const TextStyle(
  105. fontSize: 28,
  106. fontWeight: FontWeight.w700,
  107. color: AppColors.primary,
  108. ),
  109. ),
  110. ),
  111. ),
  112. const SizedBox(height: 12),
  113. const Text(
  114. '加班事由',
  115. style: TextStyle(
  116. fontSize: AppFontSizes.body,
  117. color: AppColors.textSecondary,
  118. ),
  119. ),
  120. const SizedBox(height: 8),
  121. Container(
  122. height: 88,
  123. decoration: BoxDecoration(
  124. color: AppColors.bgPage,
  125. borderRadius: BorderRadius.circular(4),
  126. ),
  127. padding: const EdgeInsets.all(12),
  128. child: TextField(
  129. controller: _reasonController,
  130. maxLines: null,
  131. expands: true,
  132. style: const TextStyle(
  133. fontSize: AppFontSizes.body,
  134. color: AppColors.textPrimary,
  135. ),
  136. decoration: InputDecoration(
  137. hintText: '请详细描述加班原因',
  138. hintStyle: TextStyle(
  139. color: AppColors.textPlaceholder,
  140. fontSize: AppFontSizes.body,
  141. ),
  142. border: InputBorder.none,
  143. isCollapsed: true,
  144. ),
  145. onChanged: ctrl.updateReason,
  146. ),
  147. ),
  148. ],
  149. ),
  150. ),
  151. ),
  152. ActionBar(
  153. showLeft: false,
  154. centerLabel: '存草稿',
  155. rightLabel: '提交审批',
  156. onCenterTap: state.isSubmitting
  157. ? null
  158. : () async {
  159. await ctrl.saveDraft();
  160. if (context.mounted) context.pop();
  161. },
  162. onRightTap: state.isSubmitting
  163. ? null
  164. : () async {
  165. final ok = await ctrl.submit();
  166. if (context.mounted && ok) context.pop();
  167. },
  168. ),
  169. ],
  170. );
  171. }
  172. void _showPicker(
  173. List<String> options,
  174. void Function(String) onPick, {
  175. String title = '请选择',
  176. }) {
  177. TDPicker.showMultiPicker(
  178. context,
  179. title: title,
  180. data: [options],
  181. onConfirm: (selected) => onPick(selected.first),
  182. );
  183. }
  184. void _pickDateTime(void Function(DateTime) onPicked, DateTime initial) {
  185. TDPicker.showDatePicker(
  186. context,
  187. title: '选择日期时间',
  188. useYear: true,
  189. useMonth: true,
  190. useDay: true,
  191. useHour: true,
  192. useMinute: true,
  193. initialDate: [
  194. initial.year,
  195. initial.month,
  196. initial.day,
  197. initial.hour,
  198. initial.minute,
  199. ],
  200. onConfirm: (selected) {
  201. onPicked(
  202. DateTime(
  203. selected['year']!,
  204. selected['month']!,
  205. selected['day']!,
  206. selected['hour']!,
  207. selected['minute']!,
  208. ),
  209. );
  210. },
  211. );
  212. }
  213. }