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 '../../shared/widgets/nav_bar_config.dart'; import '../../core/utils/date_utils.dart' as du; import '../../shared/widgets/action_bar.dart'; import '../../shared/widgets/form_section.dart'; import '../../shared/widgets/form_field_row.dart'; import '../../core/i18n/app_localizations.dart'; import 'vehicle_create_controller.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors_extension.dart'; class VehicleCreatePage extends ConsumerStatefulWidget { final String? editId; const VehicleCreatePage({super.key, this.editId}); @override ConsumerState createState() => _VehicleCreatePageState(); } class _VehicleCreatePageState extends ConsumerState { final _reasonController = TextEditingController(); final _originController = TextEditingController(); final _destinationController = TextEditingController(); final _scrollCtrl = ScrollController(); bool _showReasonError = false; // Mock vehicle pool (车牌号列表) static const _vehiclePool = [ '京A88888', '京B66666', '京C12345', '京D99999', '京E55555', ]; // Mock passengers for contact picker static const _mockContacts = [ '赵六', '钱七', '孙八', '周九', '吴十', '郑十一', '王十二', '冯十三', '陈十四', '褚十五', ]; @override void initState() { super.initState(); final state = ref.read(vehicleCreateProvider(widget.editId)); _reasonController.text = state.vehicle.reason; _originController.text = state.vehicle.origin; _destinationController.text = state.vehicle.destination; } @override void dispose() { _reasonController.dispose(); _originController.dispose(); _destinationController.dispose(); _scrollCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; final ctrl = ref.watch(vehicleCreateProvider(widget.editId).notifier); final state = ref.watch(vehicleCreateProvider(widget.editId)); final l10n = AppLocalizations.of(context); final v = state.vehicle; ref .read(navBarConfigProvider.notifier) .update( NavBarConfig( title: l10n.get('vehicleApply'), showBack: true, onBack: () { if (_hasUnsaved()) { _showConfirmDialog( l10n.get('confirmExit'), l10n.get('unsavedContentWarning'), l10n.get('continueEditing'), l10n.get('discardAndExit'), () => context.pop(), ); } else { context.pop(); } }, ), ); return PopScope( canPop: false, onPopInvokedWithResult: (didPop, _) { if (!didPop) { if (_hasUnsaved()) { _showConfirmDialog( l10n.get('confirmExit'), l10n.get('unsavedContentWarning'), l10n.get('continueEditing'), l10n.get('discardAndExit'), () => context.pop(), ); } else { context.pop(); } } }, child: Column( children: [ Expanded( child: GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: SingleChildScrollView( controller: _scrollCtrl, padding: const EdgeInsets.all(16), child: Column( children: [ FormSection( title: l10n.get('vehicleInfo'), leadingIcon: Icons.directions_car_outlined, children: [ // 车牌号 FormFieldRow( label: l10n.get('licensePlate'), value: v.vehicleId.isNotEmpty ? v.vehicleId : null, hint: l10n.get('selectLicensePlate'), onTap: () => _showVehiclePicker(ctrl), ), // 排期冲突提示 if (state.hasConflict) _buildConflictWarning(), const SizedBox(height: 16), // 用车事由 _label(l10n.get('vehicleReason'), required: true), const SizedBox(height: 8), TDTextarea( controller: _reasonController, hintText: l10n.get('enterVehicleReason'), maxLines: 4, minLines: 1, maxLength: 500, indicator: true, padding: EdgeInsets.zero, bordered: true, backgroundColor: colors.bgPage, onChanged: (val) { ctrl.updateReason(val); setState(() => _showReasonError = false); }, ), if (_showReasonError) Padding( padding: EdgeInsets.only(top: 4), child: Text( l10n.get('enterVehicleReason'), style: TextStyle( fontSize: AppFontSizes.caption, color: colors.danger, ), ), ), const SizedBox(height: 16), // 用车目的 FormFieldRow( label: l10n.get('vehiclePurpose'), value: _purposeLabel(v.purpose), hint: l10n.get('selectVehicleReason'), onTap: () => _showPurposePicker(ctrl), ), const SizedBox(height: 16), // 始发地 FormFieldRow( label: l10n.get('origin'), value: _originController.text.isNotEmpty ? _originController.text : null, hint: l10n.get('gpsLocating'), onTap: () => _showTextInput( l10n.get('origin'), (val) { _originController.text = val; ctrl.updateOrigin(val); }, initialText: _originController.text, ), ), const SizedBox(height: 16), // 目的地 FormFieldRow( label: l10n.get('destination'), value: _destinationController.text.isNotEmpty ? _destinationController.text : null, hint: l10n.get('enterDestination'), onTap: () => _showDestinationOptions(ctrl), ), const SizedBox(height: 16), // 出车时间 FormFieldRow( label: l10n.get('departTime'), value: du.DateUtils.formatDateTime(v.startTime), onTap: () => _pickDateTime(ctrl.updateStartTime, v.startTime), ), const SizedBox(height: 16), // 还车时间 FormFieldRow( label: l10n.get('returnTime'), value: du.DateUtils.formatDateTime(v.endTime), onTap: () => _pickDateTime(ctrl.updateEndTime, v.endTime), ), if (!v.endTime.isAfter(v.startTime)) Padding( padding: EdgeInsets.only(top: 4), child: Text( l10n.get('returnTimeMustLater'), style: TextStyle( fontSize: AppFontSizes.caption, color: colors.danger, ), ), ), const SizedBox(height: 16), // 同行人数 FormFieldRow( label: l10n.get('passengerCount'), value: '${v.passengerCount}${l10n.get('personUnit')}', onTap: () => _showNumberInput( l10n.get('passengerCount'), ctrl.updatePassengerCount, v.passengerCount, ), ), const SizedBox(height: 16), // 同行人 _buildPassengersSection(state, ctrl), ], ), const SizedBox(height: 80), ], ), ), ), ), ActionBar( showLeft: false, centerLabel: l10n.get('saveDraftShort'), rightLabel: l10n.get('submitApproval'), onCenterTap: state.isSubmitting ? null : () async { await ctrl.saveDraft(); if (context.mounted) { TDToast.showText(l10n.get('draftSavedToast'), context: context); context.pop(); } }, onRightTap: (state.isSubmitting || state.hasConflict) ? null : () async { final reasonOk = v.reason.trim().isNotEmpty; final vehicleOk = v.vehicleId.isNotEmpty; final timeOk = v.endTime.isAfter(v.startTime); setState(() => _showReasonError = !reasonOk); if (!reasonOk || !vehicleOk || !timeOk) { TDToast.showText(l10n.get('completeFormInfo'), context: context); return; } final ok = await ctrl.submit(); if (context.mounted) { if (ok) { TDToast.showText(l10n.get('submittedAwaitingApproval'), context: context); context.pop(); } else { TDToast.showText(l10n.get('submitFailedRetry'), context: context); } } }, ), ], ), ); } // ── 通用方法 ── bool _hasUnsaved() { final s = ref.read(vehicleCreateProvider(widget.editId)); final veh = s.vehicle; return _reasonController.text.isNotEmpty || _originController.text.isNotEmpty || _destinationController.text.isNotEmpty || veh.vehicleId.isNotEmpty || s.passengers.isNotEmpty; } void _unfocus() => FocusScope.of(context).unfocus(); void _showConfirmDialog( String title, String content, String leftText, String rightText, VoidCallback onConfirm, ) { _unfocus(); final colors = Theme.of(context).extension()!; showDialog( context: context, builder: (ctx) => TDAlertDialog( title: title, content: content, buttonStyle: TDDialogButtonStyle.text, leftBtn: TDDialogButtonOptions( title: leftText, titleColor: colors.primary, action: () => Navigator.pop(ctx), ), rightBtn: TDDialogButtonOptions( title: rightText, titleColor: colors.danger, action: () { Navigator.pop(ctx); onConfirm(); }, ), ), ); } void _showTextInput( String title, Function(String) onConfirm, { String initialText = '', }) { _unfocus(); final l10n = AppLocalizations.of(context); final c = TextEditingController(text: initialText); showGeneralDialog( context: context, pageBuilder: (ctx, animation, secondaryAnimation) => TDInputDialog( textEditingController: c, title: title, hintText: l10n.get('pleaseEnter'), leftBtn: TDDialogButtonOptions( title: l10n.get('cancel'), action: () => Navigator.pop(ctx), ), rightBtn: TDDialogButtonOptions( title: l10n.get('confirm'), action: () { onConfirm(c.text); Navigator.pop(ctx); }, ), ), ); } void _showDestinationOptions(VehicleCreateController ctrl) { _unfocus(); final l10n = AppLocalizations.of(context); showModalBottomSheet( context: context, builder: (ctx) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: Icon(Icons.edit_outlined), title: Text(l10n.get('enterDestination')), onTap: () { Navigator.pop(ctx); _showTextInput( l10n.get('destination'), (val) { _destinationController.text = val; ctrl.updateDestination(val); }, initialText: _destinationController.text, ); }, ), ListTile( leading: Icon(Icons.map_outlined), title: const Text('地图选点'), onTap: () { Navigator.pop(ctx); TDToast.showText(l10n.get('mapPickerComingSoon'), context: context); }, ), ], ), ), ); } Widget _label(String text, {bool required = false}) { final colors = Theme.of(context).extension()!; return Text.rich( TextSpan( children: [ TextSpan( text: text, style: TextStyle( fontSize: AppFontSizes.subtitle, color: colors.textSecondary, ), ), if (required) TextSpan( text: ' *', style: TextStyle( fontSize: AppFontSizes.subtitle, color: colors.danger, ), ), ], ), ); } // ── 表单字段方法 ── String _purposeLabel(String key) { final l10n = AppLocalizations.of(context); switch (key) { case 'reception': return l10n.get('customerReception'); case 'business': return l10n.get('businessTrip'); case 'official': return l10n.get('official'); default: return key; } } String _purposeKey(String label) { final l10n = AppLocalizations.of(context); if (label == l10n.get('customerReception')) return 'reception'; if (label == l10n.get('businessTrip')) return 'business'; if (label == l10n.get('official')) return 'official'; return label; } Widget _buildConflictWarning() { final l10n = AppLocalizations.of(context); final colors = Theme.of(context).extension()!; return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: colors.dangerBg, borderRadius: BorderRadius.circular(4), border: Border.all(color: colors.danger.withValues(alpha: 0.3)), ), child: Row( children: [ Icon(Icons.warning_amber_rounded, size: 16, color: colors.danger), const SizedBox(width: 8), Expanded( child: Text( l10n.get('vehicleOccupiedPeriod'), style: TextStyle( fontSize: AppFontSizes.caption, color: colors.danger, ), ), ), ], ), ); } Widget _buildPassengersSection( VehicleCreateState state, VehicleCreateController ctrl, ) { final l10n = AppLocalizations.of(context); final colors = Theme.of(context).extension()!; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( l10n.get('companion'), style: TextStyle( fontSize: AppFontSizes.subtitle, color: colors.textSecondary, ), ), GestureDetector( onTap: () => _showContactPicker(ctrl), child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), decoration: BoxDecoration( color: colors.primaryLight, borderRadius: BorderRadius.circular(16), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.person_add_alt_1, size: 14, color: colors.primary, ), SizedBox(width: 4), Text( l10n.get('add'), style: TextStyle( fontSize: AppFontSizes.caption, color: colors.primary, ), ), ], ), ), ), ], ), if (state.passengers.isNotEmpty) ...[ const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 4, children: state.passengers.map((name) { return TDTag( name, size: TDTagSize.medium, theme: TDTagTheme.primary, isLight: true, needCloseIcon: true, onCloseTap: () => ctrl.removePassenger(name), ); }).toList(), ), ], ], ); } void _showVehiclePicker(VehicleCreateController ctrl) { final l10n = AppLocalizations.of(context); _unfocus(); TDPicker.showMultiPicker( context, title: l10n.get('selectLicensePlate'), data: [_vehiclePool], onConfirm: (selected) => ctrl.updateVehicleId(selected.first), ); } void _showPurposePicker(VehicleCreateController ctrl) { final l10n = AppLocalizations.of(context); _unfocus(); final purposes = [ l10n.get('customerReception'), l10n.get('businessTrip'), l10n.get('official'), ]; TDPicker.showMultiPicker( context, title: l10n.get('selectVehicleReason'), data: [purposes], onConfirm: (selected) => ctrl.updatePurpose(_purposeKey(selected.first)), ); } void _showContactPicker(VehicleCreateController ctrl) { _unfocus(); final l10n = AppLocalizations.of(context); final state = ref.read(vehicleCreateProvider(widget.editId)); final selected = {...state.passengers}; showDialog( context: context, builder: (ctx) => TDAlertDialog( title: l10n.get('selectCompanion'), contentWidget: SizedBox( height: 300, child: ListView( children: _mockContacts.map((name) { return CheckboxListTile( title: Text(name), value: selected.contains(name), onChanged: (checked) { if (checked == true) { selected.add(name); } else { selected.remove(name); } setState(() {}); }, ); }).toList(), ), ), leftBtn: TDDialogButtonOptions( title: l10n.get('cancel'), action: () => Navigator.pop(ctx), ), rightBtn: TDDialogButtonOptions( title: l10n.get('confirm'), theme: TDButtonTheme.primary, action: () { for (final name in selected) { ctrl.addPassenger(name); } Navigator.pop(ctx); }, ), ), ); } void _showNumberInput(String title, void Function(int) onSave, int current) { _unfocus(); final l10n = AppLocalizations.of(context); final ctrl = TextEditingController(text: '$current'); showDialog( context: context, builder: (_) => TDAlertDialog( title: title, contentWidget: TDInput(controller: ctrl, hintText: '请输入数字'), leftBtn: TDDialogButtonOptions( title: l10n.get('cancel'), action: () => Navigator.pop(context), ), rightBtn: TDDialogButtonOptions( title: l10n.get('confirm'), theme: TDButtonTheme.primary, action: () { onSave(int.tryParse(ctrl.text) ?? 1); Navigator.pop(context); }, ), ), ); } void _pickDateTime(void Function(DateTime) onPicked, DateTime initial) { _unfocus(); final l10n = AppLocalizations.of(context); TDPicker.showDatePicker( context, title: l10n.get('selectDateTime'), useYear: true, useMonth: true, useDay: true, useHour: true, useMinute: true, initialDate: [ initial.year, initial.month, initial.day, initial.hour, initial.minute, ], onConfirm: (selected) { onPicked( DateTime( selected['year']!, selected['month']!, selected['day']!, selected['hour']!, selected['minute']!, ), ); }, ); } }