import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; import '../../core/theme/app_colors.dart'; import '../shell/nav_bar_config.dart'; import '../../core/utils/date_utils.dart' as du; import '../../shared/widgets/status_banner.dart'; import '../../shared/widgets/approval_timeline.dart'; import '../../shared/models/approval_status.dart'; import 'vehicle_model.dart'; import '../../core/i18n/app_localizations.dart'; import 'vehicle_list_controller.dart'; class VehicleDetailPage extends ConsumerStatefulWidget { final String id; const VehicleDetailPage({super.key, required this.id}); @override ConsumerState createState() => _VehicleDetailPageState(); } class _VehicleDetailPageState extends ConsumerState { // Return registration fields final _startOdometerCtrl = TextEditingController(); final _endOdometerCtrl = TextEditingController(); final _actualCostCtrl = TextEditingController(); final _costRemarkCtrl = TextEditingController(); DateTime? _actualReturnTime; bool _isSubmittingReturn = false; String _purposeLabel(String key) { switch (key) { case 'reception': return '客户接待'; case 'business': return '商务出行'; case 'official': return '公务'; default: return key; } } @override void dispose() { _startOdometerCtrl.dispose(); _endOdometerCtrl.dispose(); _actualCostCtrl.dispose(); _costRemarkCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final vehicle = mockVehicles.firstWhere( (e) => e.id == widget.id, orElse: () => mockVehicles.first, ); final l10n = AppLocalizations.of(context); ref .read(navBarConfigProvider.notifier) .update( NavBarConfig( title: l10n.get('vehicleDetail'), showBack: true, onBack: () => context.pop(), ), ); final (icon, color, statusText) = _statusProps(vehicle.status); return Column( children: [ Expanded( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ StatusBanner( icon: icon, statusText: statusText, subText: _statusSubText(vehicle), color: color, ), const SizedBox(height: 8), Text( '提交时间:${du.DateUtils.formatDateTime(vehicle.createTime)}', style: const TextStyle( fontSize: AppFontSizes.caption, color: AppColors.textSecondary, ), ), const SizedBox(height: 16), _buildInfoSection(vehicle), const SizedBox(height: 16), _buildMapSection(vehicle), const SizedBox(height: 16), // Return registration (only for approved) if (vehicle.status == 'approved') _buildReturnRegistration(vehicle), const SizedBox(height: 16), _buildApprovalTimeline(vehicle), ], ), ), ), _buildActionBar(context, vehicle), ], ); } Widget _buildInfoSection(VehicleModel vehicle) { return Container( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), decoration: BoxDecoration( color: AppColors.bgCard, borderRadius: BorderRadius.circular(8), ), child: Column( children: [ _infoRow('申请人', vehicle.applicantName), _infoRow('所属部门', vehicle.deptName), _infoRow('车牌号', vehicle.vehicleId.isNotEmpty ? vehicle.vehicleId : '未指定'), _infoRow('用车目的', _purposeLabel(vehicle.purpose)), _infoRow('始发地', vehicle.origin.isNotEmpty ? vehicle.origin : '未填写'), _infoRow('目的地', vehicle.destination.isNotEmpty ? vehicle.destination : '未填写'), _infoRow( '出车时间', du.DateUtils.formatDateTime(vehicle.startTime), ), _infoRow( '还车时间', du.DateUtils.formatDateTime(vehicle.endTime), ), _infoRow('同行人数', '${vehicle.passengerCount}人'), ], ), ); } Widget _infoRow(String label, String value) { return Container( height: 44, padding: const EdgeInsets.symmetric(vertical: 0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: const TextStyle( fontSize: AppFontSizes.body, color: AppColors.textSecondary, ), ), Flexible( child: Text( value, style: const TextStyle( fontSize: AppFontSizes.body, color: AppColors.textPrimary, ), textAlign: TextAlign.right, overflow: TextOverflow.ellipsis, ), ), ], ), ); } Widget _buildMapSection(VehicleModel vehicle) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.bgCard, borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '行程路线', style: TextStyle( fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), const SizedBox(height: 8), Container( height: 160, width: double.infinity, decoration: BoxDecoration( color: const Color(0xFFE8F4FD), borderRadius: BorderRadius.circular(8), ), child: Stack( children: [ Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.location_on, color: AppColors.success, size: 20), const SizedBox(width: 4), Text( vehicle.origin.isNotEmpty ? vehicle.origin : '始发地', style: const TextStyle(fontSize: 13, color: AppColors.textPrimary), ), ], ), const Padding( padding: EdgeInsets.symmetric(vertical: 4), child: Icon(Icons.arrow_downward, color: AppColors.primary, size: 18), ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.location_on, color: AppColors.danger, size: 20), const SizedBox(width: 4), Text( vehicle.destination.isNotEmpty ? vehicle.destination : '目的地', style: const TextStyle(fontSize: 13, color: AppColors.textPrimary), ), ], ), ], ), ), Positioned( bottom: 8, right: 8, child: GestureDetector( onTap: () { TDToast.showText('导航即将开放', context: context); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: AppColors.primary, borderRadius: BorderRadius.circular(12), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.navigation, size: 14, color: Colors.white), SizedBox(width: 4), Text( '导航', style: TextStyle(fontSize: 12, color: Colors.white), ), ], ), ), ), ), ], ), ), ], ), ); } Widget _buildReturnRegistration(VehicleModel vehicle) { final isEndOdometerValid = _endOdometerCtrl.text.isNotEmpty && (double.tryParse(_endOdometerCtrl.text) ?? 0) >= (double.tryParse(_startOdometerCtrl.text) ?? 0); return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.bgCard, borderRadius: BorderRadius.circular(8), border: Border.all(color: AppColors.primary.withValues(alpha: 0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.drive_eta, size: 20, color: AppColors.primary), const SizedBox(width: 8), const Text( '还车登记', style: TextStyle( fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), ], ), const SizedBox(height: 16), // 实还时间 GestureDetector( onTap: () => _pickReturnDateTime(vehicle), child: Container( height: 44, padding: const EdgeInsets.symmetric(horizontal: 0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('实还时间', style: TextStyle(fontSize: 14, color: AppColors.textSecondary)), Text( _actualReturnTime != null ? du.DateUtils.formatDateTime(_actualReturnTime!) : '请选择', style: TextStyle( fontSize: 14, color: _actualReturnTime != null ? AppColors.textPrimary : AppColors.textPlaceholder, ), ), ], ), ), ), if (_actualReturnTime != null) Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( _actualReturnTime!.isBefore(vehicle.endTime) ? '提示:提前还车' : _actualReturnTime!.isAfter(vehicle.endTime) ? '警告:已超出原计划还车时间' : '', style: TextStyle( fontSize: AppFontSizes.caption, color: _actualReturnTime!.isAfter(vehicle.endTime) ? AppColors.danger : AppColors.textSecondary, ), ), ), const SizedBox(height: 8), // 出车前里程 TDInput( controller: _startOdometerCtrl, hintText: '出车前里程(公里)', inputType: TextInputType.number, onChanged: (v) => setState(() {}), ), const SizedBox(height: 8), // 还车后里程 TDInput( controller: _endOdometerCtrl, hintText: '还车后里程(公里)', inputType: TextInputType.number, onChanged: (v) => setState(() {}), ), if (_endOdometerCtrl.text.isNotEmpty && !isEndOdometerValid) const Padding( padding: EdgeInsets.only(top: 4), child: Text( '还车后里程不能小于出车前里程', style: TextStyle( fontSize: AppFontSizes.caption, color: AppColors.danger, ), ), ), const SizedBox(height: 8), // 实际费用 TDInput( controller: _actualCostCtrl, hintText: '实际费用金额(元)', inputType: TextInputType.number, onChanged: (v) => setState(() {}), ), const SizedBox(height: 8), // 费用备注 TDInput( controller: _costRemarkCtrl, hintText: '费用备注(路桥费/停车费等)', inputType: TextInputType.text, onChanged: (v) => setState(() {}), ), const SizedBox(height: 16), // 确认提交按钮 SizedBox( width: double.infinity, height: 40, child: Material( color: _canSubmitReturn(isEndOdometerValid) ? AppColors.primary : AppColors.textPlaceholder, borderRadius: BorderRadius.circular(22), child: InkWell( onTap: _canSubmitReturn(isEndOdometerValid) && !_isSubmittingReturn ? _submitReturn : null, borderRadius: BorderRadius.circular(22), child: const Center( child: Text( '确认还车', style: TextStyle( fontSize: AppFontSizes.body, fontWeight: FontWeight.w500, color: Colors.white, ), ), ), ), ), ), ], ), ); } bool _canSubmitReturn(bool isEndOdometerValid) { return _actualReturnTime != null && _startOdometerCtrl.text.isNotEmpty && _endOdometerCtrl.text.isNotEmpty && isEndOdometerValid; } void _submitReturn() { final l10n = AppLocalizations.of(context); showDialog( context: context, builder: (ctx) => TDAlertDialog( title: l10n.get('confirmSubmit'), contentWidget: Text(l10n.get('submitConfirmContent')), leftBtn: TDDialogButtonOptions( title: l10n.get('cancel'), action: () => Navigator.pop(ctx), ), rightBtn: TDDialogButtonOptions( title: l10n.get('confirm'), theme: TDButtonTheme.primary, action: () { Navigator.pop(ctx); setState(() => _isSubmittingReturn = true); TDToast.showText('还车登记已提交', context: context); setState(() => _isSubmittingReturn = false); }, ), ), ); } Widget _buildApprovalTimeline(VehicleModel vehicle) { final mockRecords = _generateMockRecords(vehicle); final mockChain = ['u-mgr']; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.bgCard, borderRadius: BorderRadius.circular(8), ), child: ApprovalTimeline( records: mockRecords, chain: mockChain, currentApproverId: vehicle.status == 'pending' ? 'u-mgr' : '', ), ); } List _generateMockRecords(VehicleModel v) { if (v.status == 'draft' || v.status == 'withdrawn') return []; final records = [ ApprovalRecord( id: 'ar-${v.id}-init', bizId: v.id, bizType: 'vehicle', approverId: 'u-init', approverName: '系统', approvalLevel: 0, action: 'approve', opinion: '发起申请', approvalTime: v.createTime, ), ]; if (v.status == 'approved' || v.status == 'returned') { records.add(ApprovalRecord( id: 'ar-${v.id}-mgr', bizId: v.id, bizType: 'vehicle', approverId: 'u-mgr', approverName: '李四(经理)', approvalLevel: 1, action: 'approve', opinion: '同意', approvalTime: v.updateTime, )); } else if (v.status == 'rejected') { records.add(ApprovalRecord( id: 'ar-${v.id}-mgr', bizId: v.id, bizType: 'vehicle', approverId: 'u-mgr', approverName: '李四(经理)', approvalLevel: 1, action: 'reject', opinion: '请提供更详细的事由', approvalTime: v.updateTime, )); } return records; } void _pickReturnDateTime(VehicleModel vehicle) { final l10n = AppLocalizations.of(context); TDPicker.showDatePicker( context, title: l10n.get('selectReturnTime'), useYear: true, useMonth: true, useDay: true, useHour: true, useMinute: true, initialDate: [ DateTime.now().year, DateTime.now().month, DateTime.now().day, DateTime.now().hour, DateTime.now().minute, ], onConfirm: (selected) { setState(() { _actualReturnTime = DateTime( selected['year']!, selected['month']!, selected['day']!, selected['hour']!, selected['minute']!, ); }); }, ); } Widget _buildActionBar(BuildContext context, VehicleModel vehicle) { Widget? actionButton; if (vehicle.status == 'draft') { actionButton = _singleButton('编辑', AppColors.primary, () { context.push('/vehicle/apply', extra: vehicle.id); }); } else if (vehicle.status == 'pending') { actionButton = _singleButton('撤回申请', AppColors.primary, () { TDToast.showText('已撤回', context: context); }); } else if (vehicle.status == 'rejected') { actionButton = _singleButton('重新编辑', AppColors.primary, () { context.push('/vehicle/apply', extra: vehicle.id); }); } else if (vehicle.status == 'returned') { return Container( height: 72, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), decoration: const BoxDecoration(color: AppColors.bgCard), child: Center( child: Text( '已还车归档于 ${vehicle.actualReturnTime != null ? du.DateUtils.formatDateTime(vehicle.actualReturnTime!) : ''}', style: const TextStyle( fontSize: AppFontSizes.caption, color: AppColors.textSecondary, ), ), ), ); } if (actionButton == null) return const SizedBox.shrink(); return Container( height: 72, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), decoration: const BoxDecoration(color: AppColors.bgCard), child: Row(children: [const Spacer(), actionButton, const Spacer()]), ); } Widget _singleButton(String label, Color color, VoidCallback onTap) { return SizedBox( height: 40, child: Material( color: color, borderRadius: BorderRadius.circular(22), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(22), child: Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Text( label, style: const TextStyle( fontSize: AppFontSizes.body, fontWeight: FontWeight.w500, color: Colors.white, ), ), ), ), ), ), ); } (IconData, Color, String) _statusProps(String status) { switch (status) { case 'approved': return (Icons.check_circle, AppColors.success, '已通过'); case 'rejected': return (Icons.cancel, AppColors.danger, '已拒绝'); case 'draft': return (Icons.edit_note, AppColors.statusGray, '草稿'); case 'withdrawn': return (Icons.cancel_outlined, const Color(0xFF5A7D9A), '已撤回'); case 'returned': return (Icons.assignment_return, AppColors.primary, '已还车'); default: return (Icons.access_time, AppColors.warning, '审批中'); } } String _statusSubText(VehicleModel vehicle) { switch (vehicle.status) { case 'pending': return vehicle.approvalInstanceId.isNotEmpty ? '审批中,请耐心等待' : '等待审批'; case 'approved': return '已通过,请按时出车'; case 'returned': return vehicle.actualReturnTime != null ? '还车时间:${du.DateUtils.formatDateTime(vehicle.actualReturnTime!)}' : '已还车归档'; default: return ''; } } }