vehicle_apply_page.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:go_router/go_router.dart';
  4. import 'package:tdesign_flutter/tdesign_flutter.dart';
  5. import '../../core/theme/app_colors.dart';
  6. import '../shell/nav_bar_config.dart';
  7. import '../../core/utils/date_utils.dart' as du;
  8. import '../../shared/widgets/action_bar.dart';
  9. import '../../shared/widgets/form_section.dart';
  10. import '../../shared/widgets/form_field_row.dart';
  11. import '../../core/i18n/app_localizations.dart';
  12. import 'vehicle_apply_controller.dart';
  13. class VehicleApplyPage extends ConsumerStatefulWidget {
  14. final String? editId;
  15. const VehicleApplyPage({super.key, this.editId});
  16. @override
  17. ConsumerState<VehicleApplyPage> createState() => _VehicleApplyPageState();
  18. }
  19. class _VehicleApplyPageState extends ConsumerState<VehicleApplyPage> {
  20. static const _vehicleTypes = ['轿车', 'SUV', '商务车'];
  21. static const _purposes = ['客户接待', '商务出行', '内部公务', '其他'];
  22. @override
  23. Widget build(BuildContext context) {
  24. final ctrl = ref.watch(vehicleApplyProvider(widget.editId).notifier);
  25. final state = ref.watch(vehicleApplyProvider(widget.editId));
  26. final l10n = AppLocalizations.of(context);
  27. ref
  28. .read(navBarConfigProvider.notifier)
  29. .update(
  30. NavBarConfig(
  31. title: l10n.get('vehicleApply'),
  32. showBack: true,
  33. onBack: () => context.pop(),
  34. ),
  35. );
  36. return Column(
  37. children: [
  38. Expanded(
  39. child: SingleChildScrollView(
  40. padding: const EdgeInsets.all(16),
  41. child: Column(
  42. children: [
  43. FormSection(
  44. title: '用车信息',
  45. children: [
  46. FormFieldRow(
  47. label: '选择车辆',
  48. value: state.vehicle.vehicleType.isNotEmpty
  49. ? state.vehicle.vehicleType
  50. : null,
  51. hint: '请选择车牌',
  52. onTap: () => _showPicker(
  53. _vehicleTypes,
  54. ctrl.updateVehicleType,
  55. title: '选择车辆',
  56. ),
  57. ),
  58. // 冲突提示
  59. Container(
  60. height: 36,
  61. padding: const EdgeInsets.symmetric(horizontal: 12),
  62. decoration: BoxDecoration(
  63. color: const Color(0xFFFFF3E0),
  64. borderRadius: BorderRadius.circular(4),
  65. ),
  66. child: Row(
  67. children: [
  68. Icon(
  69. Icons.warning_amber_rounded,
  70. size: 14,
  71. color: AppColors.warning,
  72. ),
  73. const SizedBox(width: 8),
  74. const Text(
  75. '该车辆在此时段已被占用',
  76. style: TextStyle(
  77. fontSize: AppFontSizes.caption,
  78. color: AppColors.warning,
  79. ),
  80. ),
  81. ],
  82. ),
  83. ),
  84. FormFieldRow(
  85. label: '用车事由',
  86. value: state.vehicle.purpose.isNotEmpty
  87. ? state.vehicle.purpose
  88. : null,
  89. hint: '请选择或填写事由',
  90. onTap: () => _showPurposeDialog(ctrl),
  91. ),
  92. // 出发地点 (with map-pin icon)
  93. _buildIconFieldRow(
  94. '出发地点',
  95. state.vehicle.origin,
  96. 'GPS定位中…',
  97. () => _showInput('出发地点', ctrl.updateOrigin),
  98. ),
  99. // 目的地点 (with map-pin icon)
  100. _buildIconFieldRow(
  101. '目的地点',
  102. state.vehicle.destination,
  103. '请输入目的地点',
  104. () => _showInput('目的地点', ctrl.updateDestination),
  105. ),
  106. FormFieldRow(
  107. label: '乘车人数',
  108. value: state.vehicle.passengerCount > 0
  109. ? '${state.vehicle.passengerCount}人'
  110. : null,
  111. hint: '请选择',
  112. onTap: () => _showNumberInput(
  113. '乘车人数',
  114. ctrl.updatePassengerCount,
  115. state.vehicle.passengerCount,
  116. ),
  117. ),
  118. FormFieldRow(
  119. label: '开始时间',
  120. value: du.DateUtils.formatDateTime(
  121. state.vehicle.startTime,
  122. ),
  123. onTap: () => _pickDateTime(
  124. ctrl.updateStartTime,
  125. state.vehicle.startTime,
  126. ),
  127. ),
  128. FormFieldRow(
  129. label: '结束时间',
  130. value: du.DateUtils.formatDateTime(state.vehicle.endTime),
  131. onTap: () => _pickDateTime(
  132. ctrl.updateEndTime,
  133. state.vehicle.endTime,
  134. ),
  135. ),
  136. ],
  137. ),
  138. ],
  139. ),
  140. ),
  141. ),
  142. ActionBar(
  143. leftLabel: '重置',
  144. centerLabel: '存草稿',
  145. rightLabel: '提交审批',
  146. showLeft: true,
  147. onLeftTap: null,
  148. onCenterTap: state.isSubmitting
  149. ? null
  150. : () async {
  151. await ctrl.saveDraft();
  152. if (context.mounted) context.pop();
  153. },
  154. onRightTap: state.isSubmitting
  155. ? null
  156. : () async {
  157. final ok = await ctrl.submit();
  158. if (context.mounted && ok) context.pop();
  159. },
  160. ),
  161. ],
  162. );
  163. }
  164. Widget _buildIconFieldRow(
  165. String label,
  166. String value,
  167. String hint,
  168. VoidCallback onTap,
  169. ) {
  170. final hasValue = value.isNotEmpty;
  171. return GestureDetector(
  172. onTap: onTap,
  173. child: Container(
  174. height: 44,
  175. child: Row(
  176. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  177. children: [
  178. Text(
  179. label,
  180. style: const TextStyle(
  181. fontSize: AppFontSizes.body,
  182. color: AppColors.textSecondary,
  183. ),
  184. ),
  185. Row(
  186. mainAxisSize: MainAxisSize.min,
  187. children: [
  188. Icon(Icons.location_on, size: 14, color: AppColors.primary),
  189. const SizedBox(width: 4),
  190. Text(
  191. hasValue ? value : hint,
  192. style: TextStyle(
  193. fontSize: AppFontSizes.body,
  194. color: hasValue
  195. ? AppColors.textPrimary
  196. : AppColors.textPlaceholder,
  197. ),
  198. ),
  199. const SizedBox(width: 4),
  200. const Icon(
  201. Icons.chevron_right,
  202. size: 14,
  203. color: AppColors.textPlaceholder,
  204. ),
  205. ],
  206. ),
  207. ],
  208. ),
  209. ),
  210. );
  211. }
  212. void _showPicker(
  213. List<String> options,
  214. void Function(String) onPick, {
  215. String title = '请选择',
  216. }) {
  217. TDPicker.showMultiPicker(
  218. context,
  219. title: title,
  220. data: [options],
  221. onConfirm: (selected) => onPick(selected.first),
  222. );
  223. }
  224. void _showPurposeDialog(VehicleApplyController ctrl) {
  225. TDPicker.showMultiPicker(
  226. context,
  227. title: '选择用车事由',
  228. data: [_purposes],
  229. onConfirm: (selected) {
  230. ctrl.updatePurpose(selected.first);
  231. // 如果选择"其他",弹出文本输入
  232. if (selected.first == '其他') {
  233. _showInput('用车事由', ctrl.updatePurpose);
  234. }
  235. },
  236. );
  237. }
  238. void _showInput(String title, void Function(String) onSave) {
  239. final ctrl = TextEditingController();
  240. showDialog(
  241. context: context,
  242. builder: (_) => TDAlertDialog(
  243. title: title,
  244. contentWidget: TextField(
  245. controller: ctrl,
  246. decoration: InputDecoration(
  247. hintText: '请输入$title',
  248. border: const OutlineInputBorder(),
  249. ),
  250. ),
  251. leftBtn: TDDialogButtonOptions(
  252. title: '取消',
  253. action: () => Navigator.pop(context),
  254. ),
  255. rightBtn: TDDialogButtonOptions(
  256. title: '确定',
  257. theme: TDButtonTheme.primary,
  258. action: () {
  259. onSave(ctrl.text);
  260. Navigator.pop(context);
  261. },
  262. ),
  263. ),
  264. );
  265. }
  266. void _showNumberInput(String title, void Function(int) onSave, int current) {
  267. final ctrl = TextEditingController(text: '$current');
  268. showDialog(
  269. context: context,
  270. builder: (_) => TDAlertDialog(
  271. title: title,
  272. contentWidget: TextField(
  273. controller: ctrl,
  274. keyboardType: TextInputType.number,
  275. decoration: const InputDecoration(border: OutlineInputBorder()),
  276. ),
  277. leftBtn: TDDialogButtonOptions(
  278. title: '取消',
  279. action: () => Navigator.pop(context),
  280. ),
  281. rightBtn: TDDialogButtonOptions(
  282. title: '确定',
  283. theme: TDButtonTheme.primary,
  284. action: () {
  285. onSave(int.tryParse(ctrl.text) ?? 1);
  286. Navigator.pop(context);
  287. },
  288. ),
  289. ),
  290. );
  291. }
  292. void _pickDateTime(void Function(DateTime) onPicked, DateTime initial) {
  293. TDPicker.showDatePicker(
  294. context,
  295. title: '选择日期时间',
  296. useYear: true,
  297. useMonth: true,
  298. useDay: true,
  299. useHour: true,
  300. useMinute: true,
  301. initialDate: [
  302. initial.year,
  303. initial.month,
  304. initial.day,
  305. initial.hour,
  306. initial.minute,
  307. ],
  308. onConfirm: (selected) {
  309. onPicked(
  310. DateTime(
  311. selected['year']!,
  312. selected['month']!,
  313. selected['day']!,
  314. selected['hour']!,
  315. selected['minute']!,
  316. ),
  317. );
  318. },
  319. );
  320. }
  321. }