| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528 |
- 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<VehicleApplyPage> createState() => _VehicleApplyPageState();
- }
- class _VehicleApplyPageState extends ConsumerState<VehicleApplyPage> {
- 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<String> 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 = <String>{...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']!,
- ),
- );
- },
- );
- }
- }
|