overtime_detail_page.dart 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import 'package:flutter/material.dart';
  2. import 'package:go_router/go_router.dart';
  3. import 'package:flutter_riverpod/flutter_riverpod.dart';
  4. import '../../core/theme/app_colors.dart';
  5. import '../shell/nav_bar_config.dart';
  6. import '../../core/utils/date_utils.dart' as du;
  7. import '../../shared/widgets/status_banner.dart';
  8. import '../../shared/widgets/approval_timeline.dart';
  9. import 'overtime_model.dart';
  10. import '../../core/i18n/app_localizations.dart';
  11. import 'overtime_list_controller.dart';
  12. class OvertimeDetailPage extends ConsumerWidget {
  13. final String id;
  14. const OvertimeDetailPage({super.key, required this.id});
  15. @override
  16. Widget build(BuildContext context, WidgetRef ref) {
  17. final overtime = mockOvertimes.firstWhere(
  18. (e) => e.id == id,
  19. orElse: () => mockOvertimes.first,
  20. );
  21. final (icon, color, statusText) = _statusProps(overtime.status);
  22. final l10n = AppLocalizations.of(context);
  23. ref
  24. .read(navBarConfigProvider.notifier)
  25. .update(
  26. NavBarConfig(
  27. title: l10n.get('overtimeDetail'),
  28. showBack: true,
  29. onBack: () => context.pop(),
  30. ),
  31. );
  32. return Column(
  33. children: [
  34. Expanded(
  35. child: SingleChildScrollView(
  36. padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
  37. child: Column(
  38. crossAxisAlignment: CrossAxisAlignment.start,
  39. children: [
  40. StatusBanner(
  41. icon: icon,
  42. statusText: statusText,
  43. subText: _statusSubText(overtime),
  44. color: color,
  45. ),
  46. const SizedBox(height: 8),
  47. Text(
  48. '提交时间:${du.DateUtils.formatDateTime(overtime.createTime)}',
  49. style: const TextStyle(
  50. fontSize: AppFontSizes.caption,
  51. color: AppColors.textPlaceholder,
  52. ),
  53. ),
  54. const SizedBox(height: 16),
  55. _buildInfoSection(overtime),
  56. const SizedBox(height: 16),
  57. _buildReasonSection(overtime),
  58. const SizedBox(height: 16),
  59. if (overtime.approvalRecords.isNotEmpty ||
  60. overtime.approvalChain.isNotEmpty)
  61. Container(
  62. padding: const EdgeInsets.all(16),
  63. decoration: BoxDecoration(
  64. color: AppColors.bgCard,
  65. borderRadius: BorderRadius.circular(8),
  66. ),
  67. child: ApprovalTimeline(
  68. records: overtime.approvalRecords,
  69. chain: overtime.approvalChain,
  70. currentApproverId: overtime.currentApproverId,
  71. ),
  72. ),
  73. ],
  74. ),
  75. ),
  76. ),
  77. _buildActionBar(context, overtime),
  78. ],
  79. );
  80. }
  81. Widget _buildInfoSection(OvertimeModel overtime) {
  82. return Container(
  83. padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
  84. decoration: BoxDecoration(
  85. color: AppColors.bgCard,
  86. borderRadius: BorderRadius.circular(8),
  87. ),
  88. child: Column(
  89. children: [
  90. _infoRow('申请人', overtime.applicantName),
  91. _infoRow('所属部门', overtime.deptName),
  92. _infoRow('加班类型', overtime.otType),
  93. _infoRow('补偿方式', overtime.compensationType),
  94. _infoRow('净加班时长', '${overtime.otHours.toStringAsFixed(1)}小时'),
  95. _infoRow('开始时间', du.DateUtils.formatDateTime(overtime.startTime)),
  96. _infoRow('结束时间', du.DateUtils.formatDateTime(overtime.endTime)),
  97. ],
  98. ),
  99. );
  100. }
  101. Widget _infoRow(String label, String value) {
  102. return Container(
  103. height: 44,
  104. padding: const EdgeInsets.symmetric(vertical: 0),
  105. child: Row(
  106. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  107. children: [
  108. Text(
  109. label,
  110. style: const TextStyle(
  111. fontSize: AppFontSizes.body,
  112. color: AppColors.textSecondary,
  113. ),
  114. ),
  115. Text(
  116. value,
  117. style: const TextStyle(
  118. fontSize: AppFontSizes.body,
  119. color: AppColors.textPrimary,
  120. ),
  121. ),
  122. ],
  123. ),
  124. );
  125. }
  126. Widget _buildReasonSection(OvertimeModel overtime) {
  127. return Container(
  128. padding: const EdgeInsets.all(16),
  129. decoration: BoxDecoration(
  130. color: AppColors.bgCard,
  131. borderRadius: BorderRadius.circular(8),
  132. ),
  133. child: Column(
  134. crossAxisAlignment: CrossAxisAlignment.start,
  135. children: [
  136. const Text(
  137. '加班事由',
  138. style: TextStyle(
  139. fontSize: AppFontSizes.subtitle,
  140. fontWeight: FontWeight.w600,
  141. color: AppColors.textPrimary,
  142. ),
  143. ),
  144. const SizedBox(height: 8),
  145. Text(
  146. overtime.reason.isEmpty ? '无' : overtime.reason,
  147. style: const TextStyle(
  148. fontSize: AppFontSizes.body,
  149. color: AppColors.textPrimary,
  150. ),
  151. ),
  152. ],
  153. ),
  154. );
  155. }
  156. Widget _buildActionBar(BuildContext context, OvertimeModel overtime) {
  157. final showWithdraw =
  158. overtime.status == 'pending' || overtime.status == 'draft';
  159. if (!showWithdraw) return const SizedBox.shrink();
  160. return Container(
  161. height: 72,
  162. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
  163. decoration: const BoxDecoration(color: AppColors.bgCard),
  164. child: Row(
  165. children: [
  166. const Spacer(),
  167. SizedBox(
  168. height: 40,
  169. child: Material(
  170. color: AppColors.primary,
  171. borderRadius: BorderRadius.circular(22),
  172. child: InkWell(
  173. onTap: () {
  174. // 撤回逻辑,保留接口
  175. },
  176. borderRadius: BorderRadius.circular(22),
  177. child: const Center(
  178. child: Padding(
  179. padding: EdgeInsets.symmetric(horizontal: 32),
  180. child: Text(
  181. '撤回申请',
  182. style: TextStyle(
  183. fontSize: AppFontSizes.body,
  184. fontWeight: FontWeight.w500,
  185. color: Colors.white,
  186. ),
  187. ),
  188. ),
  189. ),
  190. ),
  191. ),
  192. ),
  193. const Spacer(),
  194. ],
  195. ),
  196. );
  197. }
  198. (IconData, Color, String) _statusProps(String status) {
  199. switch (status) {
  200. case 'approved':
  201. return (Icons.check_circle, AppColors.success, '已通过');
  202. case 'rejected':
  203. return (Icons.cancel, AppColors.danger, '已拒绝');
  204. case 'draft':
  205. return (Icons.edit_note, AppColors.statusGray, '草稿');
  206. case 'revoked':
  207. return (Icons.cancel_outlined, AppColors.revokedText, '已撤回');
  208. default:
  209. return (Icons.access_time, AppColors.warning, '审批中');
  210. }
  211. }
  212. String _statusSubText(OvertimeModel overtime) {
  213. switch (overtime.status) {
  214. case 'pending':
  215. return overtime.currentApproverId.isNotEmpty
  216. ? '当前审批人:${overtime.currentApproverId}'
  217. : '等待审批';
  218. case 'approved':
  219. final last = overtime.approvalRecords.isNotEmpty
  220. ? overtime.approvalRecords.last.approverName
  221. : '';
  222. return last.isNotEmpty ? '审批人:$last' : '已通过';
  223. case 'rejected':
  224. final last = overtime.approvalRecords.isNotEmpty
  225. ? overtime.approvalRecords.last.approverName
  226. : '';
  227. return last.isNotEmpty ? '拒绝人:$last' : '已拒绝';
  228. default:
  229. return '';
  230. }
  231. }
  232. }