vehicle_detail_page.dart 23 KB

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