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/theme/app_colors.dart'; import '../shell/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_apply_controller.dart'; class VehicleApplyPage extends ConsumerStatefulWidget { final String? editId; const VehicleApplyPage({super.key, this.editId}); @override ConsumerState createState() => _VehicleApplyPageState(); } class _VehicleApplyPageState extends ConsumerState { final _reasonController = TextEditingController(); final _originController = TextEditingController(); final _destinationController = TextEditingController(); 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(vehicleApplyProvider(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(); super.dispose(); } @override Widget build(BuildContext context) { final ctrl = ref.watch(vehicleApplyProvider(widget.editId).notifier); final state = ref.watch(vehicleApplyProvider(widget.editId)); final l10n = AppLocalizations.of(context); final v = state.vehicle; ref .read(navBarConfigProvider.notifier) .update( NavBarConfig( title: l10n.get('vehicleApply'), showBack: true, onBack: () => context.pop(), ), ); return Column( children: [ Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( children: [ FormSection( title: l10n.get('vehicleInfo'), children: [ // 车牌号 FormFieldRow( label: '车牌号', value: v.vehicleId.isNotEmpty ? v.vehicleId : null, hint: '请选择车牌号', onTap: () => _showVehiclePicker(ctrl), ), // 排期冲突提示 if (state.hasConflict) _buildConflictWarning(), const SizedBox(height: 8), // 用车事由 (TDInput) _buildReasonField(ctrl), const SizedBox(height: 8), // 用车目的 (TDPicker) FormFieldRow( label: '用车目的', value: _purposeLabel(v.purpose), hint: '请选择用车目的', onTap: () => _showPurposePicker(ctrl), ), const SizedBox(height: 8), // 始发地 (auto-filled, editable) _buildLocationField( label: '始发地', controller: _originController, hint: 'GPS定位中…', onChanged: ctrl.updateOrigin, showMapIcon: false, onMapTap: null, ), const SizedBox(height: 8), // 目的地 (with map icon) _buildLocationField( label: '目的地', controller: _destinationController, hint: '请输入目的地', onChanged: ctrl.updateDestination, showMapIcon: true, onMapTap: () { TDToast.showText( '地图选点即将开放', context: context, ); }, ), const SizedBox(height: 8), // 出车时间 FormFieldRow( label: '出车时间', value: du.DateUtils.formatDateTime(v.startTime), onTap: () => _pickDateTime(ctrl.updateStartTime, v.startTime), ), // 还车时间 FormFieldRow( label: '还车时间', value: du.DateUtils.formatDateTime(v.endTime), onTap: () => _pickDateTime(ctrl.updateEndTime, v.endTime), ), if (!v.endTime.isAfter(v.startTime)) const Padding( padding: EdgeInsets.only(top: 4), child: Text( '还车时间必须晚于出车时间', style: TextStyle( fontSize: AppFontSizes.caption, color: AppColors.danger, ), ), ), const SizedBox(height: 8), // 同行人数 FormFieldRow( label: '同行人数', value: '${v.passengerCount}人', onTap: () => _showNumberInput( '同行人数', ctrl.updatePassengerCount, v.passengerCount, ), ), // 同行人 _buildPassengersSection(state, ctrl), ], ), ], ), ), ), ActionBar( showLeft: false, centerLabel: '存草稿', rightLabel: '提交审批', onCenterTap: state.isSubmitting ? null : () async { await ctrl.saveDraft(); if (context.mounted) { TDToast.showText('已保存为草稿', 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('请完善表单信息', context: context); return; } final ok = await ctrl.submit(); if (context.mounted) { if (ok) { TDToast.showText('已提交,等待审批', context: context); context.pop(); } else { TDToast.showText('提交失败,请稍后重试', context: context); } } }, ), ], ); } String _purposeLabel(String key) { switch (key) { case 'reception': return '客户接待'; case 'business': return '商务出行'; case 'official': return '公务'; default: return key; } } String _purposeKey(String label) { switch (label) { case '客户接待': return 'reception'; case '商务出行': return 'business'; case '公务': return 'official'; default: return label; } } Widget _buildConflictWarning() { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: AppColors.dangerBg, borderRadius: BorderRadius.circular(4), border: Border.all(color: AppColors.danger.withValues(alpha: 0.3)), ), child: Row( children: [ const Icon(Icons.warning_amber_rounded, size: 16, color: AppColors.danger), const SizedBox(width: 8), const Expanded( child: Text( '该时段车辆已被预订,请选择其他车辆或调整时间', style: TextStyle( fontSize: AppFontSizes.caption, color: AppColors.danger, ), ), ), ], ), ); } Widget _buildReasonField(VehicleApplyController ctrl) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '用车事由', style: TextStyle( fontSize: AppFontSizes.body, color: AppColors.textSecondary, ), ), const SizedBox(height: 8), TDInput( controller: _reasonController, hintText: '请填写用车事由', onChanged: (v) { ctrl.updateReason(v); setState(() => _showReasonError = false); }, ), if (_showReasonError) const Padding( padding: EdgeInsets.only(top: 4), child: Text( '请填写用车事由', style: TextStyle( fontSize: AppFontSizes.caption, color: AppColors.danger, ), ), ), ], ); } Widget _buildLocationField({ required String label, required TextEditingController controller, required String hint, required ValueChanged onChanged, required bool showMapIcon, required VoidCallback? onMapTap, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: const TextStyle( fontSize: AppFontSizes.body, color: AppColors.textSecondary, ), ), const SizedBox(height: 8), Row( children: [ Expanded( child: TDInput( controller: controller, hintText: hint, onChanged: onChanged, ), ), if (showMapIcon) ...[ const SizedBox(width: 8), GestureDetector( onTap: onMapTap, child: Container( width: 40, height: 40, decoration: BoxDecoration( color: AppColors.primaryLight, borderRadius: BorderRadius.circular(8), ), child: const Icon( Icons.map_outlined, color: AppColors.primary, size: 22, ), ), ), ], ], ), ], ); } Widget _buildPassengersSection(VehicleApplyState state, VehicleApplyController ctrl) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( '同行人', style: TextStyle( fontSize: AppFontSizes.body, color: AppColors.textSecondary, ), ), GestureDetector( onTap: () => _showContactPicker(ctrl), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: AppColors.primaryLight, borderRadius: BorderRadius.circular(16), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.person_add_alt_1, size: 14, color: AppColors.primary), SizedBox(width: 4), Text( '添加', style: TextStyle( fontSize: AppFontSizes.caption, color: AppColors.primary, ), ), ], ), ), ), ], ), if (state.passengers.isNotEmpty) ...[ const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 4, children: state.passengers.map((name) { return Chip( label: Text(name, style: const TextStyle(fontSize: 12)), deleteIcon: const Icon(Icons.close, size: 16), onDeleted: () => ctrl.removePassenger(name), backgroundColor: AppColors.primaryLight, labelStyle: const TextStyle(color: AppColors.primary), side: BorderSide.none, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: VisualDensity.compact, ); }).toList(), ), ], ], ); } void _showVehiclePicker(VehicleApplyController ctrl) { final l10n = AppLocalizations.of(context); TDPicker.showMultiPicker( context, title: l10n.get('selectLicensePlate'), data: [_vehiclePool], onConfirm: (selected) => ctrl.updateVehicleId(selected.first), ); } void _showPurposePicker(VehicleApplyController ctrl) { final l10n = AppLocalizations.of(context); const purposes = ['客户接待', '商务出行', '公务']; TDPicker.showMultiPicker( context, title: l10n.get('selectVehicleReason'), data: [purposes], onConfirm: (selected) => ctrl.updatePurpose(_purposeKey(selected.first)), ); } void _showContactPicker(VehicleApplyController ctrl) { // Mock contact picker with multi-select via dialog final l10n = AppLocalizations.of(context); final state = ref.read(vehicleApplyProvider(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); } // Force rebuild 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) { final ctrl = TextEditingController(text: '$current'); showDialog( context: context, builder: (_) => TDAlertDialog( title: title, contentWidget: TextField( controller: ctrl, keyboardType: TextInputType.number, decoration: const InputDecoration(border: OutlineInputBorder()), ), leftBtn: TDDialogButtonOptions( title: '取消', action: () => Navigator.pop(context), ), rightBtn: TDDialogButtonOptions( title: '确定', theme: TDButtonTheme.primary, action: () { onSave(int.tryParse(ctrl.text) ?? 1); Navigator.pop(context); }, ), ), ); } void _pickDateTime(void Function(DateTime) onPicked, DateTime initial) { 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']!, ), ); }, ); } }