import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.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/navigation/host_app_channel.dart'; import '../../core/storage/draft_storage.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors_extension.dart'; import '../../shared/widgets/action_bar.dart'; import '../../shared/widgets/app_skeletons.dart'; import '../../shared/widgets/form_field_row.dart'; import '../../shared/widgets/form_section.dart'; import '../../shared/widgets/loading_dialog.dart'; import '../../shared/widgets/nav_bar_config.dart'; import '../../shared/widgets/app_input_dialog.dart'; import '../../shared/widgets/searchable_picker_sheet.dart'; import '../../shared/widgets/location_picker.dart'; import '../../shared/helpers/audit_flow_helper.dart'; import 'vehicle_apply_api.dart'; class VehicleApplyCreatePage extends ConsumerStatefulWidget { const VehicleApplyCreatePage({super.key}); @override ConsumerState createState() => _VehicleApplyCreatePageState(); } class _VehicleApplyCreatePageState extends ConsumerState { static const _draftKey = 'vehicle_apply'; // ── 基本信息 ── final _reasonController = TextEditingController(); String _originAdr = ''; String _destAdr = ''; double? _originLng; double? _originLat; double? _destLng; double? _destLat; DateTime? _startTime; DateTime? _endTime; // ── 车辆信息 ── String _licensePlate = ''; String _vehicleType = ''; String _brand = ''; int _seats = 0; // ── 车辆信息 ── int _odometerBegin = 0; // ── 驾驶员 ── String _driverName = ''; // ── 同行信息 ── final List _passengers = []; // ── 草稿 ── late Future _draftFuture; bool _draftHandled = false; // ── 参考数据 ── List _departments = []; List _employees = []; bool _firstBuild = true; bool _refDataLoading = true; String _selectedDeptId = ''; String _selectedDeptName = ''; String _selectedApplicantId = ''; String _selectedApplicantName = ''; final _scrollCtrl = ScrollController(); // Mock 车辆类型 static const _mockVehicleTypes = [ 'sedan', 'suv', 'mpv', 'van', 'truck', 'pickup', 'minibus', 'bus', ]; @override void initState() { super.initState(); SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.dark, ), ); _departments = []; _refDataLoading = true; _refDataFuture = null; _draftFuture = DraftStorage.has(_draftKey); _loadRefData(); WidgetsBinding.instance.addPostFrameCallback((_) => _checkDataReady()); } void _showDraftDialog() { final l10n = AppLocalizations.of(context); final colors = Theme.of(context).extension()!; FocusManager.instance.primaryFocus?.unfocus(); showDialog( context: context, barrierDismissible: false, builder: (ctx) => TDAlertDialog( title: l10n.get('draftFound'), content: l10n.get('draftRestorePrompt'), leftBtn: TDDialogButtonOptions( title: l10n.get('discard'), titleColor: colors.textSecondary, action: () { Navigator.pop(ctx); DraftStorage.delete(_draftKey); }, ), rightBtn: TDDialogButtonOptions( title: l10n.get('restore'), titleColor: colors.primary, action: () async { Navigator.pop(ctx); final data = await DraftStorage.load(_draftKey); if (data != null && mounted) { setState(() => _applyDraftData(data)); } }, ), ), ); } void _applyDraftData(Map data) { _reasonController.text = data['reason'] as String? ?? ''; _originAdr = data['originAdr'] as String? ?? ''; _originLat = (data['originLat'] as num?)?.toDouble(); _originLng = (data['originLng'] as num?)?.toDouble(); _destAdr = data['destAdr'] as String? ?? ''; _destLat = (data['destLat'] as num?)?.toDouble(); _destLng = (data['destLng'] as num?)?.toDouble(); final startStr = data['startTime'] as String?; _startTime = startStr != null ? DateTime.tryParse(startStr) : null; final endStr = data['endTime'] as String?; _endTime = endStr != null ? DateTime.tryParse(endStr) : null; _licensePlate = data['licensePlate'] as String? ?? ''; _vehicleType = data['vehicleType'] as String? ?? ''; _brand = data['brand'] as String? ?? ''; _odometerBegin = data['odometerBegin'] as int? ?? 0; _seats = data['seats'] as int? ?? 0; _driverName = data['driverName'] as String? ?? ''; _selectedDeptId = data['selectedDeptId'] as String? ?? ''; _selectedDeptName = data['selectedDeptName'] as String? ?? ''; _selectedApplicantId = data['selectedApplicantId'] as String? ?? ''; _selectedApplicantName = data['selectedApplicantName'] as String? ?? ''; // 恢复同行人 final pCount = data['passengerCount'] as int? ?? 0; final pName = data['passengerName'] as String? ?? ''; if (pCount > 0 && pName.isNotEmpty) { final parts = pName.split(';'); for (final part in parts) { if (part.isEmpty) continue; final idx = part.indexOf('/'); if (idx > 0) { _passengers.add( EmployeeItem( salNo: part.substring(0, idx), name: part.substring(idx + 1), ), ); } else { _passengers.add(EmployeeItem(salNo: part, name: part)); } } } } void _checkDataReady() { if (!_refDataLoading && mounted) { setState(() => _firstBuild = false); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) setState(() {}); }); } else if (mounted) { WidgetsBinding.instance.addPostFrameCallback((_) => _checkDataReady()); } } Future? _refDataFuture; Future _loadRefData({bool showLoading = false}) async { if (_refDataFuture != null) return _refDataFuture!; final completer = Completer(); _refDataFuture = completer.future; if (showLoading) { LoadingDialog.show( context, text: AppLocalizations.of(context).get('dataLoading'), ); } try { final api = ref.read(vehicleApplyApiProvider); final results = await Future.wait([ api.getDepartments(), api.getEmployees(), ]); if (!mounted) return; setState(() { _departments = results[0] as List; _employees = results[1] as List; _refDataLoading = false; _autoSelectDept(); _autoSelectApplicant(); _autoSelectPassenger(); _autoSelectDriver(); }); completer.complete(); } catch (_) { if (!mounted) { completer.complete(); return; } setState(() => _refDataLoading = false); completer.complete(); } finally { if (showLoading && mounted) LoadingDialog.hide(context); _refDataFuture = null; } } void _autoSelectDept() { if (_selectedDeptId.isNotEmpty) return; final dep = HostAppChannel.dep; if (dep.isEmpty) return; final match = _departments.where((d) => d.dep == dep); if (match.isNotEmpty) { _selectedDeptId = match.first.dep; _selectedDeptName = match.first.name; } } void _autoSelectApplicant() { if (_selectedApplicantId.isNotEmpty) return; final usr = HostAppChannel.usr; if (usr.isEmpty) return; final match = _employees.where((e) => e.salNo == usr); if (match.isNotEmpty) { _selectedApplicantId = match.first.salNo; _selectedApplicantName = match.first.name; } } void _autoSelectPassenger() { final usr = HostAppChannel.usr; if (usr.isEmpty) return; final match = _employees.where((e) => e.salNo == usr); if (match.isNotEmpty && !_passengers.any((p) => p.salNo == usr)) { _passengers.add(match.first); } } void _autoSelectDriver() { if (_driverName.isNotEmpty) return; final usr = HostAppChannel.usr; if (usr.isEmpty) return; final match = _employees.where((e) => e.salNo == usr); if (match.isNotEmpty) { _driverName = match.first.name; } } @override void dispose() { _reasonController.dispose(); _scrollCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); if (_firstBuild) { return const SkeletonFormPage( sectionRows: [4, 6, 4, 1], bottomButtonCount: 2, ); } Future.microtask( () => ref.read(pageBackProvider.notifier).state = () => _doPop(), ); return FutureBuilder( future: _draftFuture, builder: (ctx, snapshot) { final hasDraft = snapshot.hasData && snapshot.data == true; if (hasDraft && !_draftHandled) { _draftHandled = true; WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _showDraftDialog(); }); } return PopScope( canPop: false, onPopInvokedWithResult: (didPop, _) { if (didPop) return; _doPop(); }, child: Column( children: [ Expanded( child: GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: SingleChildScrollView( controller: _scrollCtrl, padding: const EdgeInsets.all(16), child: Column( children: [ _buildBasicInfo(l10n), const SizedBox(height: 16), _buildVehicleInfo(l10n), const SizedBox(height: 16), _buildTripInfo(l10n), const SizedBox(height: 16), _buildPassengerInfo(l10n), const SizedBox(height: 24), _buildPageFooter(), ], ), ), ), ), _buildBottomBar(l10n), ], ), ); }, ); } // ═══ 基本信息 ═══ Widget _buildBasicInfo(AppLocalizations l10n) { final colors = Theme.of(context).extension()!; return FormSection( title: l10n.get('basicInfo'), leadingIcon: Icons.info_outline, children: [ FormFieldRow( label: l10n.get('date'), value: _today(), readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('dep'), value: _selectedDeptId.isNotEmpty ? '$_selectedDeptId/$_selectedDeptName' : '', hint: l10n.get('pleaseSelect'), onTap: _refDataLoading ? null : () => _showDeptPicker(), ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('applicant'), required: true, value: _selectedApplicantId.isNotEmpty ? '$_selectedApplicantId/$_selectedApplicantName' : '', hint: l10n.get('pleaseSelect'), onTap: () => _showApplicantPicker(), ), 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, ), ], ); } // ═══ 行程信息 ═══ Widget _buildTripInfo(AppLocalizations l10n) { final colors = Theme.of(context).extension()!; final timeError = _startTime != null && _endTime != null && !_endTime!.isAfter(_startTime!); return FormSection( title: l10n.get('tripInfo'), leadingIcon: Icons.route_outlined, children: [ FormFieldRow( label: l10n.get('departTime'), value: _startTime != null ? _formatDateTime(_startTime!) : null, hint: l10n.get('pleaseSelect'), required: true, onTap: () => _pickDateTime((d) { setState(() => _startTime = d); }, _startTime), onClear: _startTime != null ? () => setState(() => _startTime = null) : null, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('returnTime'), value: _endTime != null ? _formatDateTime(_endTime!) : null, hint: l10n.get('pleaseSelect'), required: true, onTap: () => _pickDateTime((d) { setState(() => _endTime = d); }, _endTime), onClear: _endTime != null ? () => setState(() => _endTime = null) : null, ), if (timeError) Padding( padding: const EdgeInsets.only(top: 8), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Icon( Icons.warning_amber_rounded, size: 14, color: colors.danger, ), const SizedBox(width: 4), Text( l10n.get('returnTimeMustLater'), style: TextStyle( fontSize: AppFontSizes.caption, color: colors.danger, ), ), ], ), ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('origin'), value: _originAdr.isNotEmpty ? _originAdr : null, hint: l10n.get('pleaseSelect'), onTap: () async { final result = await LocationPicker.show( context, initialAddress: _originAdr, ); if (result != null && mounted) { setState(() { _originAdr = result.address; _originLat = result.latitude; _originLng = result.longitude; }); } }, onClear: _originAdr.isNotEmpty ? () => setState(() { _originAdr = ''; _originLat = null; _originLng = null; }) : null, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('destination'), value: _destAdr.isNotEmpty ? _destAdr : null, hint: l10n.get('pleaseSelect'), onTap: () async { final result = await LocationPicker.show( context, initialAddress: _destAdr, ); if (result != null && mounted) { setState(() { _destAdr = result.address; _destLat = result.latitude; _destLng = result.longitude; }); } }, onClear: _destAdr.isNotEmpty ? () => setState(() { _destAdr = ''; _destLat = null; _destLng = null; }) : null, ), ], ); } // ═══ 车辆信息 ═══ Widget _buildVehicleInfo(AppLocalizations l10n) { return FormSection( title: l10n.get('vehicleInfo'), leadingIcon: Icons.directions_car_outlined, children: [ FormFieldRow( label: l10n.get('licensePlate'), value: _licensePlate.isNotEmpty ? _licensePlate : null, hint: l10n.get('pleaseEnter'), required: true, onTap: () => _showTextInput( l10n.get('licensePlate'), (v) => setState(() => _licensePlate = v), initialText: _licensePlate, ), onClear: _licensePlate.isNotEmpty ? () => setState(() => _licensePlate = '') : null, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('vehicleType'), value: _vehicleType.isNotEmpty ? _vehicleTypeLabel(_vehicleType, l10n) : null, hint: l10n.get('pleaseSelect'), onTap: () => _showVehicleTypePicker(l10n), onClear: _vehicleType.isNotEmpty ? () => setState(() => _vehicleType = '') : null, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('brand'), value: _brand.isNotEmpty ? _brand : null, hint: l10n.get('pleaseEnter'), onTap: () => _showTextInput( l10n.get('brand'), (v) => setState(() => _brand = v), initialText: _brand, ), onClear: _brand.isNotEmpty ? () => setState(() => _brand = '') : null, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('seats'), value: _seats > 0 ? '$_seats' : null, hint: l10n.get('pleaseEnter'), onTap: () => _showNumberInput( l10n.get('seats'), (v) => setState(() => _seats = v), _seats, min: 1, ), onClear: _seats > 0 ? () => setState(() => _seats = 0) : null, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('odometerBegin'), value: _odometerBegin > 0 ? '$_odometerBegin' : null, hint: l10n.get('pleaseEnter'), onTap: () => _showNumberInput( l10n.get('odometerBegin'), (v) => setState(() => _odometerBegin = v), _odometerBegin, ), onClear: _odometerBegin > 0 ? () => setState(() => _odometerBegin = 0) : null, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('driverName'), value: _driverName.isNotEmpty ? _driverName : null, hint: l10n.get('pleaseSelect'), onTap: () => _showDriverPicker(), onClear: _driverName.isNotEmpty ? () => setState(() => _driverName = '') : null, ), ], ); } // ═══ 同行信息 ═══ Widget _buildPassengerInfo(AppLocalizations l10n) { final colors = Theme.of(context).extension()!; return FormSection( title: l10n.get('companionInfo'), leadingIcon: Icons.people_outline, showAction: true, actionText: l10n.get('add'), onActionTap: () => _showMultiEmployeePicker(l10n), children: [ if (_passengers.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text( l10n.get('noPassengerHint'), style: TextStyle( fontSize: AppFontSizes.subtitle, color: colors.textPlaceholder, ), ), ) else Wrap( spacing: 10, runSpacing: 10, children: _passengers.map((p) { final label = '${p.salNo}/${p.name.isNotEmpty ? p.name : p.salNo}'; return Container( padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 10, ), decoration: BoxDecoration( color: colors.bgCard, borderRadius: BorderRadius.circular(20), border: Border.all(color: colors.border, width: 0.5), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.04), blurRadius: 4, offset: const Offset(0, 1), ), ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.person_outline, size: 16, color: colors.primary), const SizedBox(width: 8), Text( label, style: TextStyle( fontSize: AppFontSizes.body, color: colors.textPrimary, ), ), const SizedBox(width: 8), GestureDetector( onTap: () { setState(() { _passengers.removeWhere((e) => e.salNo == p.salNo); }); }, child: Container( width: 20, height: 20, decoration: BoxDecoration( color: colors.bgPage, shape: BoxShape.circle, ), child: Icon( Icons.close, size: 12, color: colors.textSecondary, ), ), ), ], ), ); }).toList(), ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( l10n.get('passengerCount'), style: TextStyle( fontSize: AppFontSizes.body, fontWeight: FontWeight.w600, color: colors.textPrimary, ), ), Text( '${_passengers.length}', style: TextStyle( fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w700, color: colors.textPrimary, ), ), ], ), ], ); } // ═══ 底部操作栏 ═══ Widget _buildBottomBar(AppLocalizations l10n) { return ActionBar( showLeft: false, centerLabel: l10n.get('saveDraft'), rightLabel: l10n.get('submit'), centerTextOnly: true, onCenterTap: () async { FocusScope.of(context).unfocus(); try { await _saveDraftToStorage(); if (mounted) _forcePop(); } catch (_) { if (mounted) { TDToast.showFail(l10n.get('saveFailed'), context: context); } } }, onRightTap: () async { final err = _validate(l10n); if (err.isNotEmpty) { TDToast.showText(err.first, context: context); return; } FocusScope.of(context).unfocus(); LoadingDialog.show(context, text: l10n.get('submitting')); try { final data = _buildSubmitData(); final api = ref.read(vehicleApplyApiProvider); final billNo = await api.submit(data); await DraftStorage.delete(_draftKey); if (!mounted) return; LoadingDialog.hide(context); if (billNo != null) { final dd = data['HeadData']['YC_DD']?.toString() ?? ''; await AuditFlowHelper.handle( context: context, l10n: l10n, getConfig: () => api.getBillAuditConfig('YC'), onShSubmit: () => api.shSubmit(bilId: 'YC', bilNo: billNo, bilDd: dd), ); } if (mounted) { TDToast.showSuccess(l10n.get('billCreated'), context: context); GoRouter.of(context).go('/vehicle-apply/list'); } } catch (e) { if (mounted) LoadingDialog.hide(context); } }, ); } Map _buildSubmitData() { return { 'HeadData': { 'YC_DD': _today(), 'SAL_NO': _selectedApplicantId.isNotEmpty ? _selectedApplicantId : HostAppChannel.usr, 'DEP': _selectedDeptId, 'REASON': _reasonController.text.trim(), 'ORIGIN_ADR': _originAdr, 'ORIGIN_LONGITUDE': _originLng, 'ORIGIN_LATITUDE': _originLat, 'DEST_ADR': _destAdr, 'DEST_LONGITUDE': _destLng, 'DEST_LATITUDE': _destLat, 'PASSENGER_COUNT': _passengers.length, 'PASSENGER_NAME': _passengers .map((p) => '${p.salNo}/${p.name.isNotEmpty ? p.name : p.salNo}') .join(';'), 'LICENSEPLATE': _licensePlate, 'VEHICLE_TYPE': _vehicleType, 'BRAND': _brand, 'ODOMETER_BEGIN': _odometerBegin, 'SEATS': _seats, 'DRIVER_NAME': _driverName, 'START_TIME': _startTime?.toIso8601String(), 'END_TIME': _endTime?.toIso8601String(), 'USR': HostAppChannel.usr, }, }; } List _validate(AppLocalizations l10n) { final e = []; if (_selectedApplicantId.isEmpty) { e.add(l10n.get('selectApplicantHint')); } if (_reasonController.text.trim().isEmpty) { e.add(l10n.get('enterVehicleReason')); } if (_licensePlate.isEmpty) e.add(l10n.get('selectLicensePlateHint')); if (_startTime == null) { e.add(l10n.get('selectDepartTime')); } if (_endTime == null) { e.add(l10n.get('selectReturnTime')); } if (_startTime != null && _endTime != null && !_endTime!.isAfter(_startTime!)) { e.add(l10n.get('returnTimeMustLater')); } return e; } // ═══ 草稿 ═══ Future _saveDraftToStorage() async { if (!_hasUnsaved()) return; await DraftStorage.save(_draftKey, { 'reason': _reasonController.text, 'originAdr': _originAdr, 'originLat': _originLat, 'originLng': _originLng, 'destAdr': _destAdr, 'destLat': _destLat, 'destLng': _destLng, 'startTime': _startTime?.toIso8601String(), 'endTime': _endTime?.toIso8601String(), 'licensePlate': _licensePlate, 'vehicleType': _vehicleType, 'brand': _brand, 'odometerBegin': _odometerBegin, 'seats': _seats, 'driverName': _driverName, 'passengerCount': _passengers.length, 'passengerName': _passengers.map((p) => '${p.salNo}/${p.name}').join(';'), 'selectedDeptId': _selectedDeptId, 'selectedDeptName': _selectedDeptName, 'selectedApplicantId': _selectedApplicantId, 'selectedApplicantName': _selectedApplicantName, }); } void _doPop() { if (_hasUnsaved()) { final l10n = AppLocalizations.of(context); _showConfirmDialog( l10n.get('confirmExit'), l10n.get('unsavedContentWarning'), l10n.get('continueEditing'), l10n.get('discardAndExit'), () { DraftStorage.delete(_draftKey); _forcePop(); }, ); } else { _forcePop(); } } void _forcePop() { FocusManager.instance.primaryFocus?.unfocus(); final router = GoRouter.of(context); if (router.canPop()) { router.pop(); } else { SystemNavigator.pop(); } } bool _hasUnsaved() { // 同行人排除自动带出的当前用户 final usr = HostAppChannel.usr; final hasCustomPassengers = _passengers.any((p) => p.salNo != usr); // 驾驶员排除自动带出的当前用户 final selfEmployee = _employees.where((e) => e.salNo == usr); final isDefaultDriver = selfEmployee.isNotEmpty && _driverName == selfEmployee.first.name; return _reasonController.text.isNotEmpty || _originAdr.isNotEmpty || _destAdr.isNotEmpty || _startTime != null || _endTime != null || _licensePlate.isNotEmpty || _vehicleType.isNotEmpty || _brand.isNotEmpty || _seats > 0 || (_driverName.isNotEmpty && !isDefaultDriver) || hasCustomPassengers || _odometerBegin > 0; } void _showConfirmDialog( String title, String content, String leftText, String rightText, VoidCallback onConfirm, ) { FocusScope.of(context).unfocus(); FocusManager.instance.primaryFocus?.unfocus(); final colors = Theme.of(context).extension()!; showDialog( context: context, useRootNavigator: true, 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(); }, ), ), ); } // ═══ 弹窗方法 ═══ Future _showTextInput( String title, void Function(String) onConfirm, { String initialText = '', }) async { FocusScope.of(context).unfocus(); FocusManager.instance.primaryFocus?.unfocus(); final result = await AppInputDialog.show( context: context, title: title, initialText: initialText, ); if (result != null && mounted) { onConfirm(result); } } Future _showNumberInput( String title, void Function(int) onSave, int current, { int min = 0, }) async { FocusScope.of(context).unfocus(); FocusManager.instance.primaryFocus?.unfocus(); final result = await AppInputDialog.show( context: context, title: title, initialText: current > 0 ? '$current' : '', inputType: AppInputType.integer, min: min, ); if (result != null && mounted) { onSave(int.tryParse(result) ?? 0); } } void _pickDateTime(void Function(DateTime) onPicked, DateTime? initial) { FocusScope.of(context).unfocus(); final l10n = AppLocalizations.of(context); final now = DateTime.now(); final d = initial ?? now; TDPicker.showDatePicker( context, title: l10n.get('selectDateTime'), useYear: true, useMonth: true, useDay: true, useHour: true, useMinute: true, dateStart: [now.year - 1, 1, 1, 0, 0], dateEnd: [now.year + 10, 12, 31, 23, 59], initialDate: [d.year, d.month, d.day, d.hour, d.minute], onConfirm: (selected) { Navigator.of(context).pop(); onPicked( DateTime( selected['year']!, selected['month']!, selected['day']!, selected['hour']!, selected['minute']!, ), ); }, ); } Future _showApplicantPicker() async { FocusScope.of(context).unfocus(); FocusManager.instance.primaryFocus?.unfocus(); final l10n = AppLocalizations.of(context); final api = ref.read(vehicleApplyApiProvider); final result = await showSearchablePicker( context, title: '${l10n.get('select')}${l10n.get('applicant')}', searchHint: l10n.get('search'), loader: (keyword, page) => api.getEmployees(keyword: keyword, page: page, size: 20), labelBuilder: (e) => e.name.isEmpty ? e.salNo : '${e.salNo} ${e.name}', ); if (result != null && mounted) { setState(() { _selectedApplicantId = result.salNo; _selectedApplicantName = result.name; }); } } Future _showDeptPicker() async { FocusScope.of(context).unfocus(); FocusManager.instance.primaryFocus?.unfocus(); final l10n = AppLocalizations.of(context); final api = ref.read(vehicleApplyApiProvider); final result = await showSearchablePicker( context, title: '${l10n.get('select')}${l10n.get('applyDept')}', searchHint: l10n.get('search'), loader: (keyword, page) => api.getDepartments(keyword: keyword, page: page, size: 20), labelBuilder: (d) => d.name.isEmpty ? d.dep : '${d.dep} ${d.name}', ); if (result != null && mounted) { setState(() { _selectedDeptId = result.dep; _selectedDeptName = result.name; }); } } void _showVehicleTypePicker(AppLocalizations l10n) { FocusScope.of(context).unfocus(); FocusManager.instance.primaryFocus?.unfocus(); final labels = _mockVehicleTypes .map((t) => _vehicleTypeLabel(t, l10n)) .toList(); TDPicker.showMultiPicker( context, title: l10n.get('selectVehicleType'), data: [labels], onConfirm: (selected) { if (selected.isNotEmpty) { final idx = selected.first is int ? selected.first as int : 0; if (idx >= 0 && idx < _mockVehicleTypes.length) { setState(() => _vehicleType = _mockVehicleTypes[idx]); } } Navigator.of(context).pop(); }, ); } Future _showMultiEmployeePicker(AppLocalizations l10n) async { FocusScope.of(context).unfocus(); FocusManager.instance.primaryFocus?.unfocus(); final api = ref.read(vehicleApplyApiProvider); final selected = await showSearchableMultiPicker( context, title: '${l10n.get('select')}${l10n.get('companion')}', searchHint: l10n.get('search'), loader: (keyword, page) => api.getEmployees(keyword: keyword, page: page, size: 20), labelBuilder: (e) => e.name.isEmpty ? e.salNo : '${e.salNo} ${e.name}', ); if (mounted) { setState(() { for (final emp in selected) { if (!_passengers.any((p) => p.salNo == emp.salNo)) { _passengers.add(emp); } } }); } } Future _showDriverPicker() async { FocusManager.instance.primaryFocus?.unfocus(); final l10n = AppLocalizations.of(context); final api = ref.read(vehicleApplyApiProvider); final result = await showSearchablePicker( context, title: '${l10n.get('select')}${l10n.get('driverName')}', searchHint: l10n.get('search'), loader: (keyword, page) => api.getEmployees(keyword: keyword, page: page, size: 20), labelBuilder: (e) => e.name.isEmpty ? e.salNo : '${e.salNo} ${e.name}', ); if (result != null && mounted) { setState(() { _driverName = result.name; }); } } // ═══ 工具方法 ═══ Widget _label(String t, {bool required = false}) { final colors = Theme.of(context).extension()!; return Text.rich( TextSpan( children: [ TextSpan( text: t, style: TextStyle( fontSize: AppFontSizes.subtitle, color: colors.textSecondary, ), ), if (required) TextSpan( text: ' *', style: TextStyle( fontSize: AppFontSizes.subtitle, color: colors.danger, ), ), ], ), ); } Widget _buildPageFooter() { final l10n = AppLocalizations.of(context); final colors = Theme.of(context).extension()!; 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 _today() { final n = DateTime.now(); return '${n.year}-${n.month.toString().padLeft(2, '0')}-${n.day.toString().padLeft(2, '0')}'; } String _formatDateTime(DateTime d) { return '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')} ${d.hour.toString().padLeft(2, '0')}:${d.minute.toString().padLeft(2, '0')}'; } String _vehicleTypeLabel(String key, AppLocalizations l10n) { switch (key) { case 'sedan': return l10n.get('sedan'); case 'suv': return 'SUV'; case 'mpv': return l10n.get('businessVan'); case 'van': return l10n.get('van'); case 'truck': return l10n.get('truck'); case 'pickup': return l10n.get('pickup'); case 'minibus': return l10n.get('minibus'); case 'bus': return l10n.get('bus'); default: return key; } } }