vehicle_detail_page.dart 23 KB

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