vehicle_list_page.dart 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:go_router/go_router.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/filter_tabs.dart';
  8. import '../../shared/widgets/empty_state.dart';
  9. import '../../shared/widgets/loading_widget.dart';
  10. import '../../core/i18n/app_localizations.dart';
  11. import 'vehicle_list_controller.dart';
  12. import 'vehicle_model.dart';
  13. class VehicleListPage extends ConsumerStatefulWidget {
  14. const VehicleListPage({super.key});
  15. @override
  16. ConsumerState<VehicleListPage> createState() => _VehicleListPageState();
  17. }
  18. class _VehicleListPageState extends ConsumerState<VehicleListPage> {
  19. final _scrollCtrl = ScrollController();
  20. static const _tabs = ['全部', '草稿', '审批中', '已通过', '已拒绝', '已还车'];
  21. static const _tabStatusKeys = [
  22. '',
  23. 'draft',
  24. 'pending',
  25. 'approved',
  26. 'rejected',
  27. 'returned',
  28. ];
  29. @override
  30. void initState() {
  31. super.initState();
  32. _scrollCtrl.addListener(_onScroll);
  33. }
  34. void _onScroll() {
  35. if (_scrollCtrl.position.pixels >=
  36. _scrollCtrl.position.maxScrollExtent - 100) {
  37. ref.read(vehiclePageProvider.notifier).state++;
  38. }
  39. }
  40. @override
  41. void dispose() {
  42. _scrollCtrl.removeListener(_onScroll);
  43. _scrollCtrl.dispose();
  44. super.dispose();
  45. }
  46. @override
  47. Widget build(BuildContext context) {
  48. final status = ref.watch(vehicleStatusFilterProvider);
  49. final itemsAsync = ref.watch(vehicleListProvider);
  50. final l10n = AppLocalizations.of(context);
  51. int selectedIndex = _tabStatusKeys.indexOf(status);
  52. if (selectedIndex < 0) selectedIndex = 0;
  53. ref
  54. .read(navBarConfigProvider.notifier)
  55. .update(
  56. NavBarConfig(
  57. title: l10n.get('vehicleList'),
  58. showBack: true,
  59. onBack: () => context.pop(),
  60. ),
  61. );
  62. return Column(
  63. children: [
  64. FilterTabs(
  65. tabs: _tabs,
  66. selectedIndex: selectedIndex,
  67. onChanged: (index) {
  68. ref.read(vehicleStatusFilterProvider.notifier).state =
  69. _tabStatusKeys[index];
  70. },
  71. ),
  72. Expanded(
  73. child: itemsAsync.when(
  74. loading: () => const LoadingWidget(),
  75. error: (_, _) => const EmptyState(message: '加载失败'),
  76. data: (items) => items.isEmpty
  77. ? const EmptyState(message: '暂无用车记录')
  78. : RefreshIndicator(
  79. onRefresh: () async {
  80. ref.invalidate(vehicleListProvider);
  81. },
  82. child: ListView.builder(
  83. controller: _scrollCtrl,
  84. padding: const EdgeInsets.all(16),
  85. itemCount: items.length,
  86. itemBuilder: (_, i) => Padding(
  87. padding: const EdgeInsets.only(bottom: 16),
  88. child: _buildVehicleListItem(items[i]),
  89. ),
  90. ),
  91. ),
  92. ),
  93. ),
  94. ],
  95. );
  96. }
  97. Widget _buildVehicleListItem(VehicleModel item) {
  98. Color bg, fg;
  99. String label;
  100. switch (item.status) {
  101. case 'pending':
  102. bg = AppColors.warningBg;
  103. fg = AppColors.warning;
  104. label = '审批中';
  105. case 'approved':
  106. bg = AppColors.successBg;
  107. fg = AppColors.success;
  108. label = '已通过';
  109. case 'rejected':
  110. bg = AppColors.dangerBg;
  111. fg = AppColors.danger;
  112. label = '已拒绝';
  113. case 'draft':
  114. bg = AppColors.bgPage;
  115. fg = AppColors.statusGray;
  116. label = '草稿';
  117. case 'revoked':
  118. bg = AppColors.revokedBg;
  119. fg = AppColors.revokedText;
  120. label = '已撤回';
  121. case 'returned':
  122. bg = const Color(0xFFEDF2FC);
  123. fg = const Color(0xFF5A8CDB);
  124. label = '已还车';
  125. default:
  126. bg = AppColors.bgPage;
  127. fg = AppColors.statusGray;
  128. label = item.status;
  129. }
  130. return GestureDetector(
  131. onTap: () => context.push('/vehicle/detail/${item.id}'),
  132. child: Container(
  133. padding: const EdgeInsets.all(12),
  134. decoration: BoxDecoration(
  135. color: AppColors.bgCard,
  136. borderRadius: BorderRadius.circular(8),
  137. ),
  138. child: Column(
  139. children: [
  140. // R1: 车牌号 + 状态标签
  141. Row(
  142. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  143. children: [
  144. Text(
  145. item.licensePlate.isNotEmpty ? item.licensePlate : '未指定车辆',
  146. style: const TextStyle(
  147. fontSize: AppFontSizes.subtitle,
  148. fontWeight: FontWeight.w700,
  149. color: AppColors.textPrimary,
  150. ),
  151. ),
  152. Container(
  153. padding: const EdgeInsets.symmetric(
  154. horizontal: 8,
  155. vertical: 2,
  156. ),
  157. decoration: BoxDecoration(
  158. color: bg,
  159. borderRadius: BorderRadius.circular(4),
  160. ),
  161. child: Text(
  162. label,
  163. style: TextStyle(
  164. fontSize: AppFontSizes.caption,
  165. fontWeight: FontWeight.w500,
  166. color: fg,
  167. ),
  168. ),
  169. ),
  170. ],
  171. ),
  172. const SizedBox(height: 6),
  173. // R2: 申请单号 + 用途标签
  174. Row(
  175. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  176. children: [
  177. Text(
  178. item.applicationNo,
  179. style: const TextStyle(
  180. fontSize: AppFontSizes.caption,
  181. color: AppColors.textSecondary,
  182. ),
  183. ),
  184. Container(
  185. padding: const EdgeInsets.symmetric(
  186. horizontal: 6,
  187. vertical: 1,
  188. ),
  189. decoration: BoxDecoration(
  190. color: AppColors.primaryLight,
  191. borderRadius: BorderRadius.circular(3),
  192. ),
  193. child: Text(
  194. item.purpose.isNotEmpty ? item.purpose : '公务',
  195. style: const TextStyle(
  196. fontSize: 10,
  197. color: AppColors.primary,
  198. ),
  199. ),
  200. ),
  201. ],
  202. ),
  203. const SizedBox(height: 6),
  204. // R3: 路线 + 时间
  205. Row(
  206. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  207. children: [
  208. Flexible(
  209. child: Text(
  210. '${item.origin.isNotEmpty ? item.origin : '未知'} → ${item.destination.isNotEmpty ? item.destination : '未知'}',
  211. style: const TextStyle(
  212. fontSize: 13,
  213. color: AppColors.textSecondary,
  214. ),
  215. overflow: TextOverflow.ellipsis,
  216. ),
  217. ),
  218. const SizedBox(width: 8),
  219. Text(
  220. '${du.DateUtils.formatMonthDay(item.startTime)} ${du.DateUtils.formatTime(item.startTime)}',
  221. style: const TextStyle(
  222. fontSize: AppFontSizes.caption,
  223. color: AppColors.textPlaceholder,
  224. ),
  225. ),
  226. ],
  227. ),
  228. ],
  229. ),
  230. ),
  231. );
  232. }
  233. }