vehicle_detail_page.dart 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. import 'package:flutter/material.dart';
  2. import 'package:go_router/go_router.dart';
  3. import 'package:flutter_riverpod/flutter_riverpod.dart';
  4. import 'package:tdesign_flutter/tdesign_flutter.dart';
  5. import '../../shared/widgets/nav_bar_config.dart';
  6. import '../../core/utils/date_utils.dart' as du;
  7. import '../../shared/widgets/status_banner.dart';
  8. import '../../shared/widgets/approval_timeline.dart';
  9. import '../../shared/models/approval_status.dart';
  10. import 'vehicle_model.dart';
  11. import '../../core/i18n/app_localizations.dart';
  12. import 'vehicle_list_controller.dart';
  13. import '../../core/theme/app_colors.dart';
  14. import '../../core/theme/app_colors_extension.dart';
  15. class VehicleDetailPage extends ConsumerStatefulWidget {
  16. final String id;
  17. const VehicleDetailPage({super.key, required this.id});
  18. @override
  19. ConsumerState<VehicleDetailPage> createState() => _VehicleDetailPageState();
  20. }
  21. class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
  22. // Return registration fields
  23. final _startOdometerCtrl = TextEditingController();
  24. final _endOdometerCtrl = TextEditingController();
  25. final _actualCostCtrl = TextEditingController();
  26. final _costRemarkCtrl = TextEditingController();
  27. DateTime? _actualReturnTime;
  28. bool _isSubmittingReturn = false;
  29. String _purposeLabel(String key) {
  30. final l10n = AppLocalizations.of(context);
  31. switch (key) {
  32. case 'reception':
  33. return l10n.get('customerReception');
  34. case 'business':
  35. return l10n.get('businessTrip');
  36. case 'official':
  37. return l10n.get('official');
  38. default:
  39. return key;
  40. }
  41. }
  42. @override
  43. void dispose() {
  44. _startOdometerCtrl.dispose();
  45. _endOdometerCtrl.dispose();
  46. _actualCostCtrl.dispose();
  47. _costRemarkCtrl.dispose();
  48. super.dispose();
  49. }
  50. @override
  51. Widget build(BuildContext context) {
  52. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  53. final vehicle = mockVehicles.firstWhere(
  54. (e) => e.id == widget.id,
  55. orElse: () => mockVehicles.first,
  56. );
  57. final l10n = AppLocalizations.of(context);
  58. ref
  59. .read(navBarConfigProvider.notifier)
  60. .update(
  61. NavBarConfig(
  62. title: l10n.get('vehicleDetail'),
  63. showBack: true,
  64. onBack: () => context.pop(),
  65. ),
  66. );
  67. final (icon, color, statusText) = _statusProps(vehicle.status);
  68. return Column(
  69. children: [
  70. Expanded(
  71. child: SingleChildScrollView(
  72. padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
  73. child: Column(
  74. crossAxisAlignment: CrossAxisAlignment.start,
  75. children: [
  76. StatusBanner(
  77. icon: icon,
  78. statusText: statusText,
  79. subText: _statusSubText(vehicle),
  80. color: color,
  81. ),
  82. const SizedBox(height: 8),
  83. Text(
  84. '${l10n.get('submitTimeText')}:${du.DateUtils.formatDateTime(vehicle.createTime)}',
  85. style: TextStyle(
  86. fontSize: AppFontSizes.caption,
  87. color: colors.textSecondary,
  88. ),
  89. ),
  90. const SizedBox(height: 16),
  91. _buildInfoSection(vehicle),
  92. const SizedBox(height: 16),
  93. _buildMapSection(vehicle),
  94. const SizedBox(height: 16),
  95. // Return registration (only for approved)
  96. if (vehicle.status == 'approved')
  97. _buildReturnRegistration(vehicle),
  98. const SizedBox(height: 16),
  99. _buildApprovalTimeline(vehicle),
  100. ],
  101. ),
  102. ),
  103. ),
  104. _buildActionBar(context, vehicle),
  105. ],
  106. );
  107. }
  108. Widget _buildInfoSection(VehicleModel vehicle) {
  109. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  110. final l10n = AppLocalizations.of(context);
  111. return Container(
  112. padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
  113. decoration: BoxDecoration(
  114. color: colors.bgCard,
  115. borderRadius: BorderRadius.circular(8),
  116. ),
  117. child: Column(
  118. children: [
  119. _infoRow(l10n.get('applicant'), vehicle.applicantName),
  120. _infoRow(l10n.get('department'), vehicle.deptName),
  121. _infoRow(
  122. l10n.get('licensePlate'),
  123. vehicle.vehicleId.isNotEmpty ? vehicle.vehicleId : l10n.get('noVehicle'),
  124. ),
  125. _infoRow(l10n.get('vehiclePurpose'), _purposeLabel(vehicle.purpose)),
  126. _infoRow(l10n.get('origin'), vehicle.origin.isNotEmpty ? vehicle.origin : l10n.get('unknown')),
  127. _infoRow(
  128. l10n.get('destination'),
  129. vehicle.destination.isNotEmpty ? vehicle.destination : l10n.get('unknown'),
  130. ),
  131. _infoRow(l10n.get('departTime'), du.DateUtils.formatDateTime(vehicle.startTime)),
  132. _infoRow(l10n.get('returnTime'), du.DateUtils.formatDateTime(vehicle.endTime)),
  133. _infoRow(l10n.get('passengerCount'), '${vehicle.passengerCount}${l10n.get('personUnit')}'),
  134. ],
  135. ),
  136. );
  137. }
  138. Widget _infoRow(String label, String value) {
  139. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  140. return Container(
  141. height: 44,
  142. padding: const EdgeInsets.symmetric(vertical: 0),
  143. child: Row(
  144. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  145. children: [
  146. Text(
  147. label,
  148. style: TextStyle(
  149. fontSize: AppFontSizes.body,
  150. color: colors.textSecondary,
  151. ),
  152. ),
  153. Flexible(
  154. child: Text(
  155. value,
  156. style: TextStyle(
  157. fontSize: AppFontSizes.body,
  158. color: colors.textPrimary,
  159. ),
  160. textAlign: TextAlign.right,
  161. overflow: TextOverflow.ellipsis,
  162. ),
  163. ),
  164. ],
  165. ),
  166. );
  167. }
  168. Widget _buildMapSection(VehicleModel vehicle) {
  169. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  170. final l10n = AppLocalizations.of(context);
  171. return Container(
  172. padding: const EdgeInsets.all(16),
  173. decoration: BoxDecoration(
  174. color: colors.bgCard,
  175. borderRadius: BorderRadius.circular(8),
  176. ),
  177. child: Column(
  178. crossAxisAlignment: CrossAxisAlignment.start,
  179. children: [
  180. Text(
  181. l10n.get('tripRoute'),
  182. style: TextStyle(
  183. fontSize: AppFontSizes.subtitle,
  184. fontWeight: FontWeight.w600,
  185. color: colors.textPrimary,
  186. ),
  187. ),
  188. const SizedBox(height: 8),
  189. Container(
  190. height: 160,
  191. width: double.infinity,
  192. decoration: BoxDecoration(
  193. color: colors.infoLightBg,
  194. borderRadius: BorderRadius.circular(8),
  195. ),
  196. child: Stack(
  197. children: [
  198. Center(
  199. child: Column(
  200. mainAxisAlignment: MainAxisAlignment.center,
  201. children: [
  202. Row(
  203. mainAxisAlignment: MainAxisAlignment.center,
  204. children: [
  205. Icon(
  206. Icons.location_on,
  207. color: colors.success,
  208. size: 20,
  209. ),
  210. const SizedBox(width: 4),
  211. Text(
  212. vehicle.origin.isNotEmpty ? vehicle.origin : l10n.get('origin'),
  213. style: TextStyle(
  214. fontSize: 13,
  215. color: colors.textPrimary,
  216. ),
  217. ),
  218. ],
  219. ),
  220. Padding(
  221. padding: EdgeInsets.symmetric(vertical: 4),
  222. child: Icon(
  223. Icons.arrow_downward,
  224. color: colors.primary,
  225. size: 18,
  226. ),
  227. ),
  228. Row(
  229. mainAxisAlignment: MainAxisAlignment.center,
  230. children: [
  231. Icon(
  232. Icons.location_on,
  233. color: colors.danger,
  234. size: 20,
  235. ),
  236. const SizedBox(width: 4),
  237. Text(
  238. vehicle.destination.isNotEmpty
  239. ? vehicle.destination
  240. : l10n.get('destination'),
  241. style: TextStyle(
  242. fontSize: 13,
  243. color: colors.textPrimary,
  244. ),
  245. ),
  246. ],
  247. ),
  248. ],
  249. ),
  250. ),
  251. Positioned(
  252. bottom: 8,
  253. right: 8,
  254. child: GestureDetector(
  255. onTap: () {
  256. TDToast.showText(l10n.get('navigationComingSoon'), context: context);
  257. },
  258. child: Container(
  259. padding: const EdgeInsets.symmetric(
  260. horizontal: 10,
  261. vertical: 4,
  262. ),
  263. decoration: BoxDecoration(
  264. color: colors.primary,
  265. borderRadius: BorderRadius.circular(12),
  266. ),
  267. child: Row(
  268. mainAxisSize: MainAxisSize.min,
  269. children: [
  270. const Icon(Icons.navigation, size: 14, color: Colors.white),
  271. const SizedBox(width: 4),
  272. Text(
  273. l10n.get('navigation'),
  274. style: const TextStyle(fontSize: 12, color: Colors.white),
  275. ),
  276. ],
  277. ),
  278. ),
  279. ),
  280. ),
  281. ],
  282. ),
  283. ),
  284. ],
  285. ),
  286. );
  287. }
  288. Widget _buildReturnRegistration(VehicleModel vehicle) {
  289. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  290. final l10n = AppLocalizations.of(context);
  291. final isEndOdometerValid =
  292. _endOdometerCtrl.text.isNotEmpty &&
  293. (double.tryParse(_endOdometerCtrl.text) ?? 0) >=
  294. (double.tryParse(_startOdometerCtrl.text) ?? 0);
  295. return Container(
  296. padding: const EdgeInsets.all(16),
  297. decoration: BoxDecoration(
  298. color: colors.bgCard,
  299. borderRadius: BorderRadius.circular(8),
  300. border: Border.all(color: colors.primary.withValues(alpha: 0.3)),
  301. ),
  302. child: Column(
  303. crossAxisAlignment: CrossAxisAlignment.start,
  304. children: [
  305. Row(
  306. children: [
  307. Icon(Icons.drive_eta, size: 20, color: colors.primary),
  308. const SizedBox(width: 8),
  309. Text(
  310. l10n.get('returnCarRegister'),
  311. style: TextStyle(
  312. fontSize: AppFontSizes.subtitle,
  313. fontWeight: FontWeight.w600,
  314. color: colors.textPrimary,
  315. ),
  316. ),
  317. ],
  318. ),
  319. const SizedBox(height: 16),
  320. // 实还时间
  321. GestureDetector(
  322. onTap: () => _pickReturnDateTime(vehicle),
  323. child: Container(
  324. height: 44,
  325. padding: const EdgeInsets.symmetric(horizontal: 0),
  326. child: Row(
  327. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  328. children: [
  329. Text(
  330. l10n.get('actualReturnTime'),
  331. style: TextStyle(fontSize: 14, color: colors.textSecondary),
  332. ),
  333. Text(
  334. _actualReturnTime != null
  335. ? du.DateUtils.formatDateTime(_actualReturnTime!)
  336. : l10n.get('pleaseSelect'),
  337. style: TextStyle(
  338. fontSize: 14,
  339. color: _actualReturnTime != null
  340. ? colors.textPrimary
  341. : colors.textPlaceholder,
  342. ),
  343. ),
  344. ],
  345. ),
  346. ),
  347. ),
  348. if (_actualReturnTime != null)
  349. Padding(
  350. padding: const EdgeInsets.only(bottom: 8),
  351. child: Text(
  352. _actualReturnTime!.isBefore(vehicle.endTime)
  353. ? l10n.get('earlyReturn')
  354. : _actualReturnTime!.isAfter(vehicle.endTime)
  355. ? l10n.get('overReturnTime')
  356. : '',
  357. style: TextStyle(
  358. fontSize: AppFontSizes.caption,
  359. color: _actualReturnTime!.isAfter(vehicle.endTime)
  360. ? colors.danger
  361. : colors.textSecondary,
  362. ),
  363. ),
  364. ),
  365. const SizedBox(height: 8),
  366. // 出车前里程
  367. TDInput(
  368. controller: _startOdometerCtrl,
  369. hintText: l10n.get('mileageBefore'),
  370. inputType: TextInputType.number,
  371. onChanged: (v) => setState(() {}),
  372. ),
  373. const SizedBox(height: 8),
  374. // 还车后里程
  375. TDInput(
  376. controller: _endOdometerCtrl,
  377. hintText: l10n.get('mileageAfter'),
  378. inputType: TextInputType.number,
  379. onChanged: (v) => setState(() {}),
  380. ),
  381. if (_endOdometerCtrl.text.isNotEmpty && !isEndOdometerValid)
  382. Padding(
  383. padding: EdgeInsets.only(top: 4),
  384. child: Text(
  385. l10n.get('mileageInvalid'),
  386. style: TextStyle(
  387. fontSize: AppFontSizes.caption,
  388. color: colors.danger,
  389. ),
  390. ),
  391. ),
  392. const SizedBox(height: 8),
  393. // 实际费用
  394. TDInput(
  395. controller: _actualCostCtrl,
  396. hintText: l10n.get('actualCost'),
  397. inputType: TextInputType.number,
  398. onChanged: (v) => setState(() {}),
  399. ),
  400. const SizedBox(height: 8),
  401. // 费用备注
  402. TDInput(
  403. controller: _costRemarkCtrl,
  404. hintText: l10n.get('costRemarkLabel'),
  405. inputType: TextInputType.text,
  406. onChanged: (v) => setState(() {}),
  407. ),
  408. const SizedBox(height: 16),
  409. // 确认提交按钮
  410. SizedBox(
  411. width: double.infinity,
  412. height: 40,
  413. child: Material(
  414. color: _canSubmitReturn(isEndOdometerValid)
  415. ? colors.primary
  416. : colors.textPlaceholder,
  417. borderRadius: BorderRadius.circular(22),
  418. child: InkWell(
  419. onTap:
  420. _canSubmitReturn(isEndOdometerValid) && !_isSubmittingReturn
  421. ? _submitReturn
  422. : null,
  423. borderRadius: BorderRadius.circular(22),
  424. child: Center(
  425. child: Text(
  426. l10n.get('confirmReturnCar'),
  427. style: TextStyle(
  428. fontSize: AppFontSizes.body,
  429. fontWeight: FontWeight.w500,
  430. color: Colors.white,
  431. ),
  432. ),
  433. ),
  434. ),
  435. ),
  436. ),
  437. ],
  438. ),
  439. );
  440. }
  441. bool _canSubmitReturn(bool isEndOdometerValid) {
  442. return _actualReturnTime != null &&
  443. _startOdometerCtrl.text.isNotEmpty &&
  444. _endOdometerCtrl.text.isNotEmpty &&
  445. isEndOdometerValid;
  446. }
  447. void _submitReturn() {
  448. final l10n = AppLocalizations.of(context);
  449. showDialog(
  450. context: context,
  451. builder: (ctx) => TDAlertDialog(
  452. title: l10n.get('confirmSubmit'),
  453. contentWidget: Text(l10n.get('submitConfirmContent')),
  454. leftBtn: TDDialogButtonOptions(
  455. title: l10n.get('cancel'),
  456. action: () => Navigator.pop(ctx),
  457. ),
  458. rightBtn: TDDialogButtonOptions(
  459. title: l10n.get('confirm'),
  460. theme: TDButtonTheme.primary,
  461. action: () {
  462. Navigator.pop(ctx);
  463. setState(() => _isSubmittingReturn = true);
  464. TDToast.showText(l10n.get('returnCarSubmitted'), context: context);
  465. setState(() => _isSubmittingReturn = false);
  466. },
  467. ),
  468. ),
  469. );
  470. }
  471. Widget _buildApprovalTimeline(VehicleModel vehicle) {
  472. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  473. final mockRecords = _generateMockRecords(vehicle);
  474. final mockChain = <String>['u-mgr'];
  475. return Container(
  476. padding: const EdgeInsets.all(16),
  477. decoration: BoxDecoration(
  478. color: colors.bgCard,
  479. borderRadius: BorderRadius.circular(8),
  480. ),
  481. child: ApprovalTimeline(
  482. records: mockRecords,
  483. chain: mockChain,
  484. currentApproverId: vehicle.status == 'pending' ? 'u-mgr' : '',
  485. ),
  486. );
  487. }
  488. List<ApprovalRecord> _generateMockRecords(VehicleModel v) {
  489. if (v.status == 'draft' || v.status == 'withdrawn') return [];
  490. final records = <ApprovalRecord>[
  491. ApprovalRecord(
  492. id: 'ar-${v.id}-init',
  493. bizId: v.id,
  494. bizType: 'vehicle',
  495. approverId: 'u-init',
  496. approverName: '系统',
  497. approvalLevel: 0,
  498. action: 'approve',
  499. opinion: '发起申请',
  500. approvalTime: v.createTime,
  501. ),
  502. ];
  503. if (v.status == 'approved' || v.status == 'returned') {
  504. records.add(
  505. ApprovalRecord(
  506. id: 'ar-${v.id}-mgr',
  507. bizId: v.id,
  508. bizType: 'vehicle',
  509. approverId: 'u-mgr',
  510. approverName: '李四(经理)',
  511. approvalLevel: 1,
  512. action: 'approve',
  513. opinion: '同意',
  514. approvalTime: v.updateTime,
  515. ),
  516. );
  517. } else if (v.status == 'rejected') {
  518. records.add(
  519. ApprovalRecord(
  520. id: 'ar-${v.id}-mgr',
  521. bizId: v.id,
  522. bizType: 'vehicle',
  523. approverId: 'u-mgr',
  524. approverName: '李四(经理)',
  525. approvalLevel: 1,
  526. action: 'reject',
  527. opinion: '请提供更详细的事由',
  528. approvalTime: v.updateTime,
  529. ),
  530. );
  531. }
  532. return records;
  533. }
  534. void _pickReturnDateTime(VehicleModel vehicle) {
  535. final l10n = AppLocalizations.of(context);
  536. TDPicker.showDatePicker(
  537. context,
  538. title: l10n.get('selectReturnTime'),
  539. useYear: true,
  540. useMonth: true,
  541. useDay: true,
  542. useHour: true,
  543. useMinute: true,
  544. initialDate: [
  545. DateTime.now().year,
  546. DateTime.now().month,
  547. DateTime.now().day,
  548. DateTime.now().hour,
  549. DateTime.now().minute,
  550. ],
  551. onConfirm: (selected) {
  552. setState(() {
  553. _actualReturnTime = DateTime(
  554. selected['year']!,
  555. selected['month']!,
  556. selected['day']!,
  557. selected['hour']!,
  558. selected['minute']!,
  559. );
  560. });
  561. },
  562. );
  563. }
  564. Widget _buildActionBar(BuildContext context, VehicleModel vehicle) {
  565. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  566. final l10n = AppLocalizations.of(context);
  567. Widget? actionButton;
  568. if (vehicle.status == 'draft') {
  569. actionButton = _singleButton(l10n.get('edit'), colors.primary, () {
  570. context.push('/vehicle/apply', extra: vehicle.id);
  571. });
  572. } else if (vehicle.status == 'pending') {
  573. actionButton = _singleButton(l10n.get('withdrawApplication'), colors.primary, () {
  574. TDToast.showText(l10n.get('withdrawn'), context: context);
  575. });
  576. } else if (vehicle.status == 'rejected') {
  577. actionButton = _singleButton(l10n.get('reEdit'), colors.primary, () {
  578. context.push('/vehicle/apply', extra: vehicle.id);
  579. });
  580. } else if (vehicle.status == 'returned') {
  581. return Container(
  582. height: 72,
  583. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
  584. decoration: BoxDecoration(color: colors.bgCard),
  585. child: Center(
  586. child: Text(
  587. l10n.getString('returnCarArchivedAt', args: {'time': vehicle.actualReturnTime != null ? du.DateUtils.formatDateTime(vehicle.actualReturnTime!) : ''}),
  588. style: TextStyle(
  589. fontSize: AppFontSizes.caption,
  590. color: colors.textSecondary,
  591. ),
  592. ),
  593. ),
  594. );
  595. }
  596. if (actionButton == null) return const SizedBox.shrink();
  597. return Container(
  598. height: 72,
  599. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
  600. decoration: BoxDecoration(color: colors.bgCard),
  601. child: Row(children: [const Spacer(), actionButton, const Spacer()]),
  602. );
  603. }
  604. Widget _singleButton(String label, Color color, VoidCallback onTap) {
  605. return SizedBox(
  606. height: 40,
  607. child: Material(
  608. color: color,
  609. borderRadius: BorderRadius.circular(22),
  610. child: InkWell(
  611. onTap: onTap,
  612. borderRadius: BorderRadius.circular(22),
  613. child: Center(
  614. child: Padding(
  615. padding: const EdgeInsets.symmetric(horizontal: 32),
  616. child: Text(
  617. label,
  618. style: const TextStyle(
  619. fontSize: AppFontSizes.body,
  620. fontWeight: FontWeight.w500,
  621. color: Colors.white,
  622. ),
  623. ),
  624. ),
  625. ),
  626. ),
  627. ),
  628. );
  629. }
  630. (IconData, Color, String) _statusProps(String status) {
  631. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  632. final l10n = AppLocalizations.of(context);
  633. switch (status) {
  634. case 'approved':
  635. return (Icons.check_circle, colors.success, l10n.get('approved'));
  636. case 'rejected':
  637. return (Icons.cancel, colors.danger, l10n.get('rejected'));
  638. case 'draft':
  639. return (Icons.edit_note, colors.statusGray, l10n.get('draft'));
  640. case 'withdrawn':
  641. return (Icons.cancel_outlined, colors.withdrawnText, l10n.get('withdrawn'));
  642. case 'returned':
  643. return (Icons.assignment_return, colors.primary, l10n.get('returned'));
  644. default:
  645. return (Icons.access_time, colors.warning, l10n.get('pending'));
  646. }
  647. }
  648. String _statusSubText(VehicleModel vehicle) {
  649. switch (vehicle.status) {
  650. case 'pending':
  651. return vehicle.approvalInstanceId.isNotEmpty ? '审批中,请耐心等待' : '等待审批';
  652. case 'approved':
  653. return '已通过,请按时出车';
  654. case 'returned':
  655. return vehicle.actualReturnTime != null
  656. ? '还车时间:${du.DateUtils.formatDateTime(vehicle.actualReturnTime!)}'
  657. : '已还车归档';
  658. default:
  659. return '';
  660. }
  661. }
  662. }