import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; import 'package:easy_refresh/easy_refresh.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors_extension.dart'; import '../../core/auth/role_provider.dart'; import '../../shared/widgets/nav_bar_config.dart'; import '../../core/utils/date_utils.dart' as du; import '../../shared/widgets/empty_state.dart'; import '../../shared/widgets/skeleton_list_card.dart'; import '../../shared/widgets/list_filter_panel.dart'; import '../../shared/widgets/list_footer.dart'; import '../../shared/widgets/status_tag.dart'; import '../../core/i18n/app_localizations.dart'; import 'vehicle_list_controller.dart'; import 'vehicle_model.dart'; final _scopeProvider = StateProvider((ref) => 'my'); class VehicleListPage extends ConsumerStatefulWidget { const VehicleListPage({super.key}); @override ConsumerState createState() => _VehicleListPageState(); } class _VehicleListPageState extends ConsumerState with TickerProviderStateMixin { List _getTabLabels(AppLocalizations l10n) => [ l10n.get('all'), l10n.get('draft'), l10n.get('pending'), l10n.get('approved'), l10n.get('rejected'), l10n.get('withdrawn'), l10n.get('returned'), ]; static const _tabKeys = [ '', 'draft', 'pending', 'approved', 'rejected', 'withdrawn', 'returned', ]; late final TabController _tabCtrl; @override void initState() { super.initState(); _tabCtrl = TabController(length: _tabKeys.length, vsync: this); _tabCtrl.addListener(() { if (!_tabCtrl.indexIsChanging) { ref.read(vehicleStatusFilterProvider.notifier).state = _tabKeys[_tabCtrl.index]; } }); } @override void dispose() { _tabCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final status = ref.watch(vehicleStatusFilterProvider); final dateStart = ref.watch(vehicleDateStartProvider); final dateEnd = ref.watch(vehicleDateEndProvider); final purposeFilter = ref.watch(vehiclePurposeFilterProvider); final l10n = AppLocalizations.of(context); final colors = Theme.of(context).extension()!; final isManager = ref.watch(isManagerProvider); // Sync TabController with external filter changes final targetIdx = _tabKeys.indexOf(status); if (targetIdx >= 0 && _tabCtrl.index != targetIdx && !_tabCtrl.indexIsChanging) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _tabCtrl.animateTo(targetIdx); }); } final filterGroups = [ FilterGroup( title: l10n.get('filterDateRange'), type: FilterGroupType.dateRange, sections: [ FilterSection( label: l10n.get('filterDateRange'), type: FilterSectionType.dateRange, startDate: dateStart, endDate: dateEnd, onStartChanged: (v) => ref.read(vehicleDateStartProvider.notifier).state = v, onEndChanged: (v) => ref.read(vehicleDateEndProvider.notifier).state = v, ), ], ), FilterGroup( title: l10n.get('other'), type: FilterGroupType.other, sections: [ FilterSection( label: l10n.get('vehiclePurpose'), type: FilterSectionType.singleSelect, options: [ FilterOption( value: 'reception', label: l10n.get('customerReception'), ), FilterOption(value: 'business', label: l10n.get('businessTrip')), FilterOption(value: 'official', label: l10n.get('official')), ], selectedValue: purposeFilter, onChanged: (v) => ref.read(vehiclePurposeFilterProvider.notifier).state = v, ), ], ), ]; final hasFilter = ListFilterPanel.hasActiveFilter(filterGroups); final filterVersion = Object.hash(dateStart, dateEnd, purposeFilter); final now = DateTime.now(); void onFilterReset() { ref.read(vehicleDateStartProvider.notifier).state = null; ref.read(vehicleDateEndProvider.notifier).state = null; ref.read(vehiclePurposeFilterProvider.notifier).state = null; } ref .read(navBarConfigProvider.notifier) .update( NavBarConfig( title: l10n.get('vehicleList'), showBack: true, showRight: true, rightWidget: GestureDetector( onTap: () => ListFilterPanel.show( context, groups: filterGroups, onReset: onFilterReset, onConfirm: () {}, defaultStartDate: DateTime(now.year, now.month, 1), defaultEndDate: DateTime(now.year, now.month, now.day), ), child: Stack( clipBehavior: Clip.none, children: [ Icon( TDIcons.filter, size: 22, color: hasFilter ? colors.primary : colors.textPrimary, ), if (hasFilter) Positioned( right: -2, top: -2, child: Container( width: 6, height: 6, decoration: BoxDecoration( color: colors.danger, shape: BoxShape.circle, ), ), ), ], ), ), hasFilter: hasFilter, filterVersion: filterVersion, onBack: () => context.pop(), ), ); return Column( children: [ if (isManager) Container( color: colors.bgCard, padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: _buildScopeChip(colors), ), Container( color: colors.bgCard, padding: EdgeInsets.zero, child: TDSearchBar( placeHolder: l10n.get('searchVehicle'), needCancel: true, style: TDSearchStyle.round, ), ), Container( color: colors.bgCard, padding: const EdgeInsets.symmetric(horizontal: 8), child: TDTabBar( tabs: _getTabLabels(l10n).map((l) => TDTab(text: l)).toList(), controller: _tabCtrl, isScrollable: true, labelColor: colors.primary, unselectedLabelColor: colors.textSecondary, outlineType: TDTabBarOutlineType.filled, showIndicator: true, indicatorColor: colors.primary, indicatorHeight: 3, dividerHeight: 0, labelPadding: const EdgeInsets.symmetric(horizontal: 12), onTap: (index) { ref.read(vehicleStatusFilterProvider.notifier).state = _tabKeys[index]; }, ), ), Expanded( child: Container( color: colors.bgPage, child: TabBarView( controller: _tabCtrl, children: List.generate(_tabKeys.length, (tabIdx) { return _buildTabContent(tabIdx); }), ), ), ), ], ); } Widget _buildScopeChip(AppColorsExtension colors) { final scope = ref.watch(_scopeProvider); final l10n = AppLocalizations.of(context); return Row( children: [ GestureDetector( onTap: () => ref.read(_scopeProvider.notifier).state = 'my', child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), decoration: BoxDecoration( color: scope == 'my' ? colors.primary : colors.bgPage, borderRadius: BorderRadius.circular(16), border: scope == 'my' ? null : Border.all(color: colors.border), ), child: Text( l10n.get('scopeMyApplications'), style: TextStyle( fontSize: 13, color: scope == 'my' ? colors.bgCard : colors.textSecondary, ), ), ), ), const SizedBox(width: 8), GestureDetector( onTap: () => ref.read(_scopeProvider.notifier).state = 'sub', child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), decoration: BoxDecoration( color: scope == 'sub' ? colors.primary : colors.bgPage, borderRadius: BorderRadius.circular(16), border: scope == 'sub' ? null : Border.all(color: colors.border), ), child: Text( l10n.get('scopeSubordinates'), style: TextStyle( fontSize: 13, color: scope == 'sub' ? colors.bgCard : colors.textSecondary, ), ), ), ), ], ); } Widget _buildTabContent(int tabIdx) { return _VehicleTabContent(statusKey: _tabKeys[tabIdx]); } } class _VehicleTabContent extends ConsumerWidget { final String statusKey; const _VehicleTabContent({required this.statusKey}); @override Widget build(BuildContext context, WidgetRef ref) { final itemsAsync = ref.watch(vehicleListProvider(statusKey)); final scope = ref.watch(_scopeProvider); if (itemsAsync.isLoading && !itemsAsync.hasValue) { return SkeletonLoadingList( cardBuilder: () => const SkeletonVehicleCard(), ); } return EasyRefresh( header: TDRefreshHeader(), onRefresh: () async { ref.read(vehicleRefreshProvider.notifier).state++; }, child: _buildContent(itemsAsync, context, ref, scope), ); } Widget _buildContent( AsyncValue> itemsAsync, BuildContext context, WidgetRef ref, String scope, ) { final l10n = AppLocalizations.of(context); final isSub = scope == 'sub'; if (itemsAsync.isReloading) { final oldItems = itemsAsync.valueOrNull ?? []; if (oldItems.isEmpty) { return ListView( children: [ const SizedBox(height: 120), EmptyState(message: l10n.get('noVehicles')), ], ); } return ListView.builder( padding: const EdgeInsets.all(16), itemCount: oldItems.length, itemBuilder: (_, i) { final card = _buildVehicleListItem( context, oldItems[i], isSub: isSub, ); if (isSub && oldItems[i].status == 'pending') { return Padding( padding: const EdgeInsets.only(bottom: 16), child: _buildSwipeApprove(card, oldItems[i].id), ); } return Padding( padding: const EdgeInsets.only(bottom: 16), child: card, ); }, ); } if (itemsAsync.hasError) { return ListView( children: [ const SizedBox(height: 120), EmptyState(message: l10n.get('loadFailed')), ], ); } final items = itemsAsync.requireValue; if (items.isEmpty) { return ListView( children: [ const SizedBox(height: 120), EmptyState(message: l10n.get('noVehicles')), ], ); } return ListView.builder( padding: const EdgeInsets.all(16), itemCount: items.length + 1, itemBuilder: (_, i) { if (i == items.length) return ListFooter(itemCount: items.length); final card = _buildVehicleListItem(context, items[i], isSub: isSub); if (isSub && items[i].status == 'pending') { return Padding( padding: const EdgeInsets.only(bottom: 16), child: _buildSwipeApprove(card, items[i].id), ); } return Padding(padding: const EdgeInsets.only(bottom: 16), child: card); }, ); } Widget _buildVehicleListItem( BuildContext context, VehicleModel item, { bool isSub = false, }) { final l10n = AppLocalizations.of(context); final colors = Theme.of(context).extension()!; return GestureDetector( onTap: () => context.push('/vehicle/detail/${item.id}'), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.bgCard, borderRadius: BorderRadius.circular(8), ), child: Column( children: [ // R1: 车牌号 + 状态标签 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( item.licensePlate.isNotEmpty ? item.licensePlate : '未指定车辆', style: TextStyle( fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w700, color: colors.textPrimary, ), ), StatusTag.fromStatus(item.status, l10n), ], ), if (isSub) ...[ const SizedBox(height: 4), Align( alignment: Alignment.centerLeft, child: Text( '申请人: ${item.applicantName} · ${item.deptName}', style: TextStyle( fontSize: AppFontSizes.caption, color: colors.textPlaceholder, ), ), ), ], const SizedBox(height: 6), // R2: 申请单号 + 用途标签 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( item.applicationNo, style: TextStyle( fontSize: AppFontSizes.caption, color: colors.textSecondary, ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 1, ), decoration: BoxDecoration( color: colors.primaryLight, borderRadius: BorderRadius.circular(3), ), child: Text( item.purpose.isNotEmpty ? item.purpose : '公务', style: TextStyle(fontSize: 10, color: colors.primary), ), ), ], ), const SizedBox(height: 6), // R3: 路线 + 时间 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: Text( '${item.origin.isNotEmpty ? item.origin : '未知'} → ${item.destination.isNotEmpty ? item.destination : '未知'}', style: TextStyle(fontSize: 13, color: colors.textSecondary), overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 8), Text( '${du.DateUtils.formatMonthDay(item.startTime)} ${du.DateUtils.formatTime(item.startTime)}', style: TextStyle( fontSize: AppFontSizes.caption, color: colors.textPlaceholder, ), ), ], ), ], ), ), ); } Widget _buildSwipeApprove(Widget card, String itemId) { return Builder( builder: (ctx) { final screenWidth = MediaQuery.of(ctx).size.width; return TDSwipeCell( groupTag: 'vehicle_approve', right: TDSwipeCellPanel( extentRatio: 100 / screenWidth, children: [ TDSwipeCellAction( label: '', backgroundColor: Colors.transparent, builder: (_) => Container( margin: const EdgeInsets.symmetric( horizontal: 4, vertical: 8, ), decoration: BoxDecoration( color: Colors.green, borderRadius: BorderRadius.circular(8), ), alignment: Alignment.center, padding: const EdgeInsets.symmetric(horizontal: 12), child: const Text( '一键同意', style: TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600, ), ), ), onPressed: (_) async { final confirmed = await showDialog( context: ctx, builder: (dCtx) => TDAlertDialog( title: '确认审批', content: '确认同意该用车申请?', leftBtn: TDDialogButtonOptions( title: '取消', action: () => Navigator.of(dCtx).pop(false), ), rightBtn: TDDialogButtonOptions( title: '确认', action: () => Navigator.of(dCtx).pop(true), ), ), ); if (confirmed == true) { // TODO: 接入实际审批 API if (ctx.mounted) { TDToast.showSuccess('已审批通过', context: ctx); } } }, ), ], ), cell: card, ); }, ); } }