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 '../../core/i18n/app_localizations.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors_extension.dart'; import '../../core/utils/amount_utils.dart'; import '../../core/utils/date_utils.dart' as du; import '../../shared/widgets/app_skeletons.dart'; import '../../shared/widgets/form_field_row.dart'; import '../../shared/widgets/form_section.dart'; import 'vehicle_api.dart'; import 'vehicle_list_controller.dart'; import 'vehicle_model.dart'; class VehicleDetailPage extends ConsumerStatefulWidget { final String billNo; const VehicleDetailPage({super.key, required this.billNo}); @override ConsumerState createState() => _VehicleDetailPageState(); } class _VehicleDetailPageState extends ConsumerState { bool _loading = true; String? _error; VehicleModel? _data; // ── 还车登记字段 ── DateTime? _returnTime; final _odometerController = TextEditingController(); final _amtnController = TextEditingController(); final _remarkController = TextEditingController(); bool _isSubmittingReturn = false; @override void initState() { super.initState(); _loadData(); } @override void dispose() { _odometerController.dispose(); _amtnController.dispose(); _remarkController.dispose(); super.dispose(); } Future _loadData() async { setState(() { _loading = true; _error = null; }); try { // 先用 mock 数据匹配 billNo final match = mockVehicles.where((e) => e.ycNo == widget.billNo); if (match.isNotEmpty) { if (mounted) { setState(() { _data = match.first; _loading = false; }); } } else { // 尝试 API final api = ref.read(vehicleApiProvider); final detail = await api.fetchDetail(widget.billNo); if (mounted) setState(() { _data = detail; _loading = false; }); } // 单据状态 try { final api = ref.read(vehicleApiProvider); await api.getBillStatus(widget.billNo); } catch (_) { /* 非关键 */ } } catch (e) { if (mounted) setState(() { _error = e.toString(); _loading = false; }); } } @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; final l10n = AppLocalizations.of(context); if (_loading) return const SkeletonDetailPage(); if (_error != null) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.error_outline, size: 48, color: colors.danger), const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Text(_error!, textAlign: TextAlign.center, style: TextStyle(fontSize: AppFontSizes.body, color: colors.textSecondary)), ), const SizedBox(height: 16), TDButton(text: l10n.get('retry'), size: TDButtonSize.medium, onTap: _loadData), ], ), ); } final app = _data!; return Column( children: [ Expanded( child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(16), child: Column( children: [ _buildBasicInfoSection(app, l10n, colors), const SizedBox(height: 16), _buildVehicleInfoSection(app, l10n, colors), if (app.isReturn) _buildReturnedInfoSection(app, l10n, colors) else if (app.isAuditApproved) _buildReturnRegistrationSection(l10n, colors), const SizedBox(height: 24), _buildPageFooter(colors), ], ), ), ), _buildActionBar(app, l10n, colors), ], ); } // ═══ 状态 tag ═══ Widget _buildStatusTag(AppLocalizations l10n) { final colors = Theme.of(context).extension()!; final approved = _data?.isAuditApproved ?? false; final returned = _data?.isReturn ?? false; if (returned) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: colors.infoText.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), border: Border.all(color: colors.infoText.withValues(alpha: 0.3), width: 0.5), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.assignment_return, size: 18, color: colors.infoText), const SizedBox(width: 4), Text('已还车', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.infoText)), ], ), ); } if (approved) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: colors.success.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), border: Border.all(color: colors.success.withValues(alpha: 0.3), width: 0.5), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.check_circle_outline, size: 18, color: colors.success), const SizedBox(width: 4), Text(l10n.get('statusApproved'), style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.success)), ], ), ); } return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: colors.statusGray.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), border: Border.all(color: colors.statusGray.withValues(alpha: 0.3), width: 0.5), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.access_time, size: 18, color: colors.statusGray), const SizedBox(width: 4), Text(l10n.get('statusPending'), style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.statusGray)), ], ), ); } // ═══ 基本信息 ═══ Widget _buildBasicInfoSection(VehicleModel app, AppLocalizations l10n, AppColorsExtension colors) { return FormSection( title: l10n.get('basicInfo'), leadingIcon: Icons.info_outline, trailing: _buildStatusTag(l10n), children: [ FormFieldRow(label: '用车申请单号', value: app.ycNo, readOnly: true, showArrow: false), const SizedBox(height: 16), FormFieldRow(label: l10n.get('date'), value: app.ycDd != null ? du.DateUtils.formatDate(app.ycDd!) : '-', readOnly: true, showArrow: false), const SizedBox(height: 16), FormFieldRow(label: '申请人', value: app.applicantName.isNotEmpty ? app.applicantName : app.salNo, readOnly: true, showArrow: false), const SizedBox(height: 16), FormFieldRow( label: l10n.get('applyDept'), value: app.dep.isNotEmpty ? '${app.dep}${app.deptName.isNotEmpty ? '/${app.deptName}' : ''}' : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow(label: '用车目的', value: _purposeLabel(app.purpose, l10n), readOnly: true, showArrow: false), const SizedBox(height: 16), FormFieldRow(label: '用车事由', value: app.reason.isNotEmpty ? app.reason : '-', readOnly: true, showArrow: false, bold: true, showMoreOnOverflow: true), const SizedBox(height: 16), FormFieldRow(label: '始发地', value: app.origin.isNotEmpty ? app.origin : '-', readOnly: true, showArrow: false, showMoreOnOverflow: true), const SizedBox(height: 16), FormFieldRow(label: '目的地', value: app.destinAdr.isNotEmpty ? app.destinAdr : '-', readOnly: true, showArrow: false, showMoreOnOverflow: true), const SizedBox(height: 16), FormFieldRow( label: '预计出车时间', value: app.startTime != null ? du.DateUtils.formatDateTime(app.startTime!) : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: '预计还车时间', value: app.endTime != null ? du.DateUtils.formatDateTime(app.endTime!) : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow(label: '同行人数', value: '${app.passengerCount}', readOnly: true, showArrow: false), const SizedBox(height: 16), FormFieldRow(label: '同行人姓名', value: app.passengerName.isNotEmpty ? app.passengerName : '-', readOnly: true, showArrow: false, showMoreOnOverflow: true), ], ); } // ═══ 车辆信息 ═══ Widget _buildVehicleInfoSection(VehicleModel app, AppLocalizations l10n, AppColorsExtension colors) { return FormSection( title: '车辆信息', leadingIcon: Icons.directions_car_outlined, children: [ FormFieldRow(label: '车牌号', value: app.licensePlate.isNotEmpty ? app.licensePlate : '-', readOnly: true, showArrow: false), const SizedBox(height: 16), FormFieldRow(label: '车辆类型', value: _vehicleTypeLabel(app.vehicleType, l10n), readOnly: true, showArrow: false), const SizedBox(height: 16), FormFieldRow(label: '品牌型号', value: app.brand.isNotEmpty ? app.brand : '-', readOnly: true, showArrow: false), const SizedBox(height: 16), FormFieldRow(label: '核定座位数', value: app.seats > 0 ? '${app.seats}' : '-', readOnly: true, showArrow: false), const SizedBox(height: 16), FormFieldRow(label: '默认驾驶员', value: app.driverName.isNotEmpty ? app.driverName : '-', readOnly: true, showArrow: false), ], ); } // ═══ 已还车信息 ═══ Widget _buildReturnedInfoSection(VehicleModel app, AppLocalizations l10n, AppColorsExtension colors) { return FormSection( title: '还车信息', leadingIcon: Icons.assignment_return, children: [ FormFieldRow( label: '实际归还时间', value: app.returnTime != null ? du.DateUtils.formatDateTime(app.returnTime!) : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow(label: '里程表读数', value: app.odometer.toString(), readOnly: true, showArrow: false), const SizedBox(height: 16), FormFieldRow(label: '实际费用', value: formatAmount(app.amtn), readOnly: true, showArrow: false, bold: true), const SizedBox(height: 16), FormFieldRow(label: '费用备注', value: app.remark.isNotEmpty ? app.remark : '-', readOnly: true, showArrow: false, showMoreOnOverflow: true), ], ); } // ═══ 还车登记 ═══ Widget _buildReturnRegistrationSection(AppLocalizations l10n, AppColorsExtension colors) { return FormSection( title: '还车登记', leadingIcon: Icons.drive_eta, children: [ FormFieldRow( label: '实际归还时间', value: _returnTime != null ? du.DateUtils.formatDateTime(_returnTime!) : null, hint: l10n.get('pleaseSelect'), onTap: () => _pickReturnDateTime(), ), const SizedBox(height: 16), _label('里程表读数', colors), const SizedBox(height: 8), TDInput( controller: _odometerController, hintText: '请输入里程表读数', inputType: const TextInputType.numberWithOptions(decimal: true), backgroundColor: colors.bgPage, ), const SizedBox(height: 16), _label('路桥费/停车费等实际费用', colors), const SizedBox(height: 8), TDInput( controller: _amtnController, hintText: '请输入实际费用', inputType: const TextInputType.numberWithOptions(decimal: true), backgroundColor: colors.bgPage, onChanged: (_) => setState(() {}), ), if (_amtnController.text.isNotEmpty) ...[ const SizedBox(height: 4), Align( alignment: Alignment.centerRight, child: Text( '金额: ${formatAmount(double.tryParse(_amtnController.text) ?? 0)}', style: TextStyle(fontSize: AppFontSizes.caption, color: colors.amountPrimary, fontWeight: FontWeight.w600), ), ), ], const SizedBox(height: 16), _label('费用明细备注', colors), const SizedBox(height: 8), TDTextarea( controller: _remarkController, hintText: '请输入费用明细备注', maxLines: 3, minLines: 1, maxLength: 500, indicator: true, padding: EdgeInsets.zero, bordered: true, backgroundColor: colors.bgPage, ), const SizedBox(height: 16), SizedBox( width: double.infinity, height: 40, child: Material( color: _canSubmitReturn ? colors.primary : colors.textPlaceholder, borderRadius: BorderRadius.circular(22), child: InkWell( onTap: _canSubmitReturn && !_isSubmittingReturn ? _submitReturn : null, borderRadius: BorderRadius.circular(22), child: Center( child: Text('确认还车', style: TextStyle(fontSize: AppFontSizes.body, fontWeight: FontWeight.w500, color: Colors.white)), ), ), ), ), ], ); } bool get _canSubmitReturn => _returnTime != null && _odometerController.text.isNotEmpty; void _pickReturnDateTime() { final d = DateTime.now(); TDPicker.showDatePicker( context, title: '选择实际归还时间', useYear: true, useMonth: true, useDay: true, useHour: true, useMinute: true, initialDate: [d.year, d.month, d.day, d.hour, d.minute], onConfirm: (selected) { setState(() { _returnTime = DateTime(selected['year']!, selected['month']!, selected['day']!, selected['hour']!, selected['minute']!); }); }, ); } Future _submitReturn() async { final l10n = AppLocalizations.of(context); final confirmed = await showDialog( context: context, builder: (ctx) => TDAlertDialog( title: '确认还车', content: '确认提交还车登记?', leftBtn: TDDialogButtonOptions(title: l10n.get('cancel'), action: () => Navigator.pop(ctx, false)), rightBtn: TDDialogButtonOptions(title: l10n.get('confirm'), theme: TDButtonTheme.primary, action: () => Navigator.pop(ctx, true)), ), ); if (confirmed != true) return; setState(() => _isSubmittingReturn = true); try { final api = ref.read(vehicleApiProvider); await api.submitReturn(widget.billNo, { 'returnTime': _returnTime?.toIso8601String(), 'odometer': double.tryParse(_odometerController.text) ?? 0, 'amtn': double.tryParse(_amtnController.text) ?? 0, 'remark': _remarkController.text, }); if (mounted) { TDToast.showSuccess('还车登记成功', context: context); _loadData(); } } catch (_) { if (mounted) TDToast.showFail('还车登记失败', context: context); } finally { if (mounted) setState(() => _isSubmittingReturn = false); } } // ═══ 底部操作栏 ═══ Widget _buildActionBar(VehicleModel app, AppLocalizations l10n, AppColorsExtension colors) { // 只有待审核状态显示编辑/撤回按钮 if (app.isReturn) return const SizedBox.shrink(); if (!app.isAuditApproved) { // 待审核:可编辑/撤回 return Container( height: 72, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), decoration: BoxDecoration( color: colors.bgCard, border: Border(top: BorderSide(color: colors.border, width: 0.5)), ), child: Row( children: [ Expanded( child: SizedBox( height: 40, child: Material( color: colors.bgCard, borderRadius: BorderRadius.circular(22), child: InkWell( onTap: () { TDToast.showText('已撤回', context: context); if (context.mounted) GoRouter.of(context).pop(); }, borderRadius: BorderRadius.circular(22), child: Center( child: Text('撤回', style: TextStyle(fontSize: AppFontSizes.body, fontWeight: FontWeight.w500, color: colors.danger)), ), ), ), ), ), ], ), ); } return const SizedBox.shrink(); } // ═══ 工具方法 ═══ Widget _label(String t, AppColorsExtension colors) { return Text(t, style: TextStyle(fontSize: AppFontSizes.subtitle, color: colors.textSecondary)); } Widget _buildPageFooter(AppColorsExtension colors) { final l10n = AppLocalizations.of(context); return Center( child: Padding( padding: const EdgeInsets.only(bottom: 16), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.rocket_launch_outlined, size: 16, color: colors.textPlaceholder), const SizedBox(width: 6), Text(l10n.get('pageFooter'), style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textPlaceholder)), ], ), ), ); } String _purposeLabel(String key, AppLocalizations l10n) { switch (key) { case 'reception': return l10n.get('customerReception'); case 'business': return l10n.get('businessTrip'); case 'official': return l10n.get('official'); case 'other': return l10n.get('other'); default: return key; } } String _vehicleTypeLabel(String key, AppLocalizations l10n) { switch (key) { case 'sedan': return '轿车'; case 'suv': return 'SUV'; case 'mpv': return '商务车'; case 'van': return '面包车'; default: return key; } } }