vehicle_apply_page.dart 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:go_router/go_router.dart';
  4. import '../../core/utils/date_utils.dart' as du;
  5. import '../../core/utils/responsive.dart';
  6. import '../../shared/widgets/form_section.dart';
  7. import '../../shared/widgets/form_field_row.dart';
  8. import 'vehicle_apply_controller.dart';
  9. class VehicleApplyPage extends ConsumerStatefulWidget {
  10. final String? editId;
  11. const VehicleApplyPage({super.key, this.editId});
  12. @override
  13. ConsumerState<VehicleApplyPage> createState() => _VehicleApplyPageState();
  14. }
  15. class _VehicleApplyPageState extends ConsumerState<VehicleApplyPage> {
  16. static const _vehicleTypes = ['轿车', '商务车', '中巴', '大巴', '货车'];
  17. static const _purposes = ['客户接待', '商务出行', '货物运输', '其他'];
  18. @override
  19. Widget build(BuildContext context) {
  20. final ctrl = ref.watch(vehicleApplyProvider(widget.editId).notifier);
  21. final state = ref.watch(vehicleApplyProvider(widget.editId));
  22. final r = ResponsiveHelper.of(context);
  23. return Scaffold(
  24. appBar: AppBar(title: Text(widget.editId != null ? '编辑用车' : '用车申请')),
  25. body: Column(
  26. children: [
  27. Expanded(
  28. child: Align(alignment: Alignment.topCenter,
  29. child: ConstrainedBox(
  30. constraints: BoxConstraints(maxWidth: r.formMaxWidth),
  31. child: SingleChildScrollView(
  32. padding: const EdgeInsets.symmetric(vertical: 8),
  33. child: FormSection(
  34. title: '用车信息',
  35. children: [
  36. FormFieldRow(label: '用车目的', value: state.vehicle.purpose, hint: '请选择', onTap: () => _showPicker(_purposes, ctrl.updatePurpose)),
  37. FormFieldRow(label: '车型', value: state.vehicle.vehicleType, onTap: () => _showPicker(_vehicleTypes, ctrl.updateVehicleType)),
  38. FormFieldRow(label: '开始时间', value: du.DateUtils.formatDateTime(state.vehicle.startTime), onTap: () => _pickDateTime(ctrl.updateStartTime, state.vehicle.startTime)),
  39. FormFieldRow(label: '结束时间', value: du.DateUtils.formatDateTime(state.vehicle.endTime), onTap: () => _pickDateTime(ctrl.updateEndTime, state.vehicle.endTime)),
  40. FormFieldRow(label: '出发地', value: state.vehicle.origin, hint: '请输入', onTap: () => _showInput('出发地', ctrl.updateOrigin)),
  41. FormFieldRow(label: '目的地', value: state.vehicle.destination, hint: '请输入', onTap: () => _showInput('目的地', ctrl.updateDestination)),
  42. FormFieldRow(label: '乘车人数', value: '${state.vehicle.passengerCount}人', onTap: () => _showNumberInput('乘车人数', ctrl.updatePassengerCount, state.vehicle.passengerCount)),
  43. FormFieldRow(label: '驾驶员', value: state.vehicle.driver, hint: '请输入', onTap: () => _showInput('驾驶员', ctrl.updateDriver)),
  44. FormFieldRow(label: '用车事由', value: state.vehicle.reason, hint: '请输入事由', onTap: () => _showInput('用车事由', ctrl.updateReason)),
  45. ],
  46. ),
  47. ),
  48. ),
  49. ),
  50. ),
  51. _buildBottomButtons(ctrl, state),
  52. ],
  53. ),
  54. );
  55. }
  56. void _showInput(String title, void Function(String) onSave) {
  57. final ctrl = TextEditingController();
  58. showDialog(
  59. context: context,
  60. builder: (_) => AlertDialog(
  61. title: Text(title),
  62. content: TextField(controller: ctrl, decoration: InputDecoration(hintText: '请输入$title', border: const OutlineInputBorder())),
  63. actions: [
  64. TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消')),
  65. TextButton(onPressed: () { onSave(ctrl.text); Navigator.pop(context); }, child: const Text('确定')),
  66. ],
  67. ),
  68. );
  69. }
  70. void _showNumberInput(String title, void Function(int) onSave, int current) {
  71. final ctrl = TextEditingController(text: '$current');
  72. showDialog(
  73. context: context,
  74. builder: (_) => AlertDialog(
  75. title: Text(title),
  76. content: TextField(controller: ctrl, keyboardType: TextInputType.number, decoration: const InputDecoration(border: OutlineInputBorder())),
  77. actions: [
  78. TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消')),
  79. TextButton(onPressed: () { onSave(int.tryParse(ctrl.text) ?? 1); Navigator.pop(context); }, child: const Text('确定')),
  80. ],
  81. ),
  82. );
  83. }
  84. void _showPicker(List<String> options, void Function(String) onPick) {
  85. showModalBottomSheet(
  86. context: context,
  87. builder: (_) => Column(
  88. mainAxisSize: MainAxisSize.min,
  89. children: options.map((t) => ListTile(title: Text(t), onTap: () { onPick(t); Navigator.pop(context); })).toList(),
  90. ),
  91. );
  92. }
  93. Future<void> _pickDateTime(void Function(DateTime) onPicked, DateTime initial) async {
  94. final date = await showDatePicker(context: context, initialDate: initial, firstDate: DateTime(2020), lastDate: DateTime(2030));
  95. if (date == null || !context.mounted) return;
  96. final time = await showTimePicker(context: context, initialTime: TimeOfDay.fromDateTime(initial));
  97. if (time == null) return;
  98. onPicked(DateTime(date.year, date.month, date.day, time.hour, time.minute));
  99. }
  100. Widget _buildBottomButtons(VehicleApplyController ctrl, VehicleApplyState state) {
  101. return Container(
  102. padding: const EdgeInsets.all(12),
  103. decoration: BoxDecoration(color: Colors.white, boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 4, offset: const Offset(0, -1))]),
  104. child: Row(
  105. children: [
  106. Expanded(child: OutlinedButton(
  107. onPressed: state.isSubmitting ? null : () async { await ctrl.saveDraft(); if (context.mounted) context.pop(); },
  108. child: const Text('存草稿'),
  109. )),
  110. const SizedBox(width: 12),
  111. Expanded(flex: 2, child: ElevatedButton(
  112. onPressed: state.isSubmitting ? null : () async { final ok = await ctrl.submit(); if (context.mounted && ok) context.pop(); },
  113. child: state.isSubmitting ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : const Text('提交申请'),
  114. )),
  115. ],
  116. ),
  117. );
  118. }
  119. }