vehicle_detail_page.dart 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  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 'vehicle_model.dart';
  10. import '../../core/i18n/app_localizations.dart';
  11. import 'vehicle_list_controller.dart';
  12. class VehicleDetailPage extends ConsumerWidget {
  13. final String id;
  14. const VehicleDetailPage({super.key, required this.id});
  15. @override
  16. Widget build(BuildContext context, WidgetRef ref) {
  17. final vehicle = mockVehicles.firstWhere(
  18. (e) => e.id == id,
  19. orElse: () => mockVehicles.first,
  20. );
  21. final (icon, color, statusText) = _statusProps(vehicle.status);
  22. final l10n = AppLocalizations.of(context);
  23. ref
  24. .read(navBarConfigProvider.notifier)
  25. .update(
  26. NavBarConfig(
  27. title: l10n.get('vehicleDetail'),
  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(vehicle),
  44. color: color,
  45. ),
  46. const SizedBox(height: 8),
  47. Text(
  48. '提交时间:${du.DateUtils.formatDateTime(vehicle.createTime)}',
  49. style: const TextStyle(
  50. fontSize: AppFontSizes.caption,
  51. color: AppColors.textSecondary,
  52. ),
  53. ),
  54. const SizedBox(height: 16),
  55. _buildInfoSection(vehicle),
  56. const SizedBox(height: 16),
  57. _buildMapSection(),
  58. const SizedBox(height: 16),
  59. if (vehicle.approvalRecords.isNotEmpty ||
  60. vehicle.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: vehicle.approvalRecords,
  69. chain: vehicle.approvalChain,
  70. currentApproverId: vehicle.currentApproverId,
  71. ),
  72. ),
  73. ],
  74. ),
  75. ),
  76. ),
  77. _buildActionBar(context, vehicle),
  78. ],
  79. );
  80. }
  81. Widget _buildInfoSection(VehicleModel vehicle) {
  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('申请人', vehicle.applicantName),
  91. _infoRow('所属部门', vehicle.deptName),
  92. _infoRow('用车车型', vehicle.vehicleType),
  93. _infoRow('用车目的', vehicle.purpose),
  94. _infoRow('出发地点', vehicle.origin),
  95. _infoRow('目的地点', vehicle.destination),
  96. _infoRow(
  97. '预计时间',
  98. '${du.DateUtils.formatDateTime(vehicle.startTime)} ~ ${du.DateUtils.formatTime(vehicle.endTime)}',
  99. ),
  100. _infoRow('乘车人数', '${vehicle.passengerCount}人'),
  101. _infoRow('预估里程', '${vehicle.estimatedMileage.toStringAsFixed(0)}公里'),
  102. ],
  103. ),
  104. );
  105. }
  106. Widget _infoRow(String label, String value) {
  107. return Container(
  108. height: 44,
  109. padding: const EdgeInsets.symmetric(vertical: 0),
  110. child: Row(
  111. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  112. children: [
  113. Text(
  114. label,
  115. style: const TextStyle(
  116. fontSize: AppFontSizes.body,
  117. color: AppColors.textSecondary,
  118. ),
  119. ),
  120. Flexible(
  121. child: Text(
  122. value,
  123. style: const TextStyle(
  124. fontSize: AppFontSizes.body,
  125. color: AppColors.textPrimary,
  126. ),
  127. textAlign: TextAlign.right,
  128. overflow: TextOverflow.ellipsis,
  129. ),
  130. ),
  131. ],
  132. ),
  133. );
  134. }
  135. Widget _buildMapSection() {
  136. return Container(
  137. padding: const EdgeInsets.all(16),
  138. decoration: BoxDecoration(
  139. color: AppColors.bgCard,
  140. borderRadius: BorderRadius.circular(8),
  141. ),
  142. child: Column(
  143. crossAxisAlignment: CrossAxisAlignment.start,
  144. children: [
  145. const Text(
  146. '行程预览',
  147. style: TextStyle(
  148. fontSize: AppFontSizes.subtitle,
  149. fontWeight: FontWeight.w600,
  150. color: AppColors.textPrimary,
  151. ),
  152. ),
  153. const SizedBox(height: 8),
  154. Container(
  155. height: 160,
  156. width: double.infinity,
  157. decoration: BoxDecoration(
  158. color: const Color(0xFFE8F4FD),
  159. borderRadius: BorderRadius.circular(8),
  160. ),
  161. child: const Center(
  162. child: Text(
  163. '地图路线预览',
  164. style: TextStyle(
  165. fontSize: AppFontSizes.body,
  166. color: AppColors.primary,
  167. ),
  168. ),
  169. ),
  170. ),
  171. ],
  172. ),
  173. );
  174. }
  175. Widget _buildActionBar(BuildContext context, VehicleModel vehicle) {
  176. // 根据状态显示操作按钮
  177. Widget? actionButton;
  178. if (vehicle.status == 'pending' || vehicle.status == 'draft') {
  179. actionButton = _singleButton('撤回申请', AppColors.primary, () {});
  180. } else if (vehicle.status == 'approved') {
  181. // 已通过可显示确认还车
  182. actionButton = _singleButton('确认还车并登记', AppColors.primary, () {});
  183. }
  184. if (actionButton == null) return const SizedBox.shrink();
  185. return Container(
  186. height: 72,
  187. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
  188. decoration: const BoxDecoration(color: AppColors.bgCard),
  189. child: Row(children: [const Spacer(), actionButton, const Spacer()]),
  190. );
  191. }
  192. Widget _singleButton(String label, Color color, VoidCallback onTap) {
  193. return SizedBox(
  194. height: 40,
  195. child: Material(
  196. color: color,
  197. borderRadius: BorderRadius.circular(22),
  198. child: InkWell(
  199. onTap: onTap,
  200. borderRadius: BorderRadius.circular(22),
  201. child: Center(
  202. child: Padding(
  203. padding: const EdgeInsets.symmetric(horizontal: 32),
  204. child: Text(
  205. label,
  206. style: const TextStyle(
  207. fontSize: AppFontSizes.body,
  208. fontWeight: FontWeight.w500,
  209. color: Colors.white,
  210. ),
  211. ),
  212. ),
  213. ),
  214. ),
  215. ),
  216. );
  217. }
  218. (IconData, Color, String) _statusProps(String status) {
  219. switch (status) {
  220. case 'approved':
  221. return (Icons.check_circle, AppColors.success, '已通过');
  222. case 'rejected':
  223. return (Icons.cancel, AppColors.danger, '已拒绝');
  224. case 'draft':
  225. return (Icons.edit_note, AppColors.statusGray, '草稿');
  226. case 'revoked':
  227. return (Icons.cancel_outlined, AppColors.revokedText, '已撤回');
  228. case 'returned':
  229. return (Icons.assignment_return, AppColors.primary, '已还车');
  230. default:
  231. return (Icons.access_time, AppColors.warning, '审批中');
  232. }
  233. }
  234. String _statusSubText(VehicleModel vehicle) {
  235. switch (vehicle.status) {
  236. case 'pending':
  237. return vehicle.currentApproverId.isNotEmpty
  238. ? '当前审批人:${vehicle.currentApproverId}'
  239. : '等待审批';
  240. case 'approved':
  241. return vehicle.currentApproverId.isNotEmpty
  242. ? '当前审批人:${vehicle.currentApproverId}'
  243. : '已通过';
  244. default:
  245. return '';
  246. }
  247. }
  248. }