expense_apply_import_page.dart 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:tdesign_flutter/tdesign_flutter.dart';
  4. import '../../shared/widgets/nav_bar_config.dart';
  5. import '../../core/i18n/app_localizations.dart';
  6. import '../../core/theme/app_colors_extension.dart';
  7. import 'expense_api.dart';
  8. /// 可导入的费用申请明细项
  9. class ImportableItem {
  10. final String aeNo;
  11. final String aeDd;
  12. final String reason;
  13. final double headAmtnYj;
  14. final int itm;
  15. final String sqMan;
  16. final String sqName;
  17. final String typeNo;
  18. final String typeName;
  19. final double amtnYj;
  20. final String accNo;
  21. final String accName;
  22. final String dep;
  23. final String depName;
  24. final String objNo;
  25. final String objName;
  26. final String startDd;
  27. final String endDd;
  28. final String rem;
  29. bool selected = false;
  30. ImportableItem({
  31. required this.aeNo, required this.aeDd, required this.reason,
  32. required this.headAmtnYj, required this.itm, required this.sqMan, required this.sqName,
  33. required this.typeNo, required this.typeName, required this.amtnYj,
  34. required this.accNo, required this.accName, required this.dep,
  35. required this.depName, required this.objNo, required this.objName,
  36. required this.startDd, required this.endDd, required this.rem,
  37. });
  38. factory ImportableItem.fromJson(Map<String, dynamic> json) => ImportableItem(
  39. aeNo: json['aeNo'] as String? ?? '',
  40. aeDd: _fmtDate(json['aeDd'] as String?),
  41. reason: json['reason'] as String? ?? '',
  42. headAmtnYj: (json['headAmtnYj'] as num?)?.toDouble() ?? 0,
  43. itm: json['itm'] as int? ?? 0,
  44. sqMan: json['sqMan'] as String? ?? '',
  45. sqName: json['sqName'] as String? ?? '',
  46. typeNo: json['typeNo'] as String? ?? '',
  47. typeName: json['typeName'] as String? ?? '',
  48. amtnYj: (json['amtnYj'] as num?)?.toDouble() ?? 0,
  49. accNo: json['accNo'] as String? ?? '',
  50. accName: json['accName'] as String? ?? '',
  51. dep: json['dep'] as String? ?? '',
  52. depName: json['depName'] as String? ?? '',
  53. objNo: json['objNo'] as String? ?? '',
  54. objName: json['objName'] as String? ?? '',
  55. startDd: _fmtDate(json['startDd'] as String?),
  56. endDd: _fmtDate(json['endDd'] as String?),
  57. rem: json['rem'] as String? ?? '',
  58. );
  59. static String _fmtDate(String? raw) {
  60. if (raw == null || raw.isEmpty) return '';
  61. try {
  62. final d = DateTime.parse(raw);
  63. return '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
  64. } catch (_) {
  65. return raw;
  66. }
  67. }
  68. }
  69. class ExpenseApplyImportPage extends ConsumerStatefulWidget {
  70. const ExpenseApplyImportPage({super.key});
  71. @override
  72. ConsumerState<ExpenseApplyImportPage> createState() => _ExpenseApplyImportPageState();
  73. }
  74. class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage> {
  75. final _aeNoCtrl = TextEditingController();
  76. final _startDateCtrl = TextEditingController();
  77. final _endDateCtrl = TextEditingController();
  78. List<ImportableItem> _items = [];
  79. bool _loading = false;
  80. bool _hasMore = true;
  81. int _page = 1;
  82. late final ScrollController _scrollCtrl;
  83. @override
  84. void initState() {
  85. super.initState();
  86. _scrollCtrl = ScrollController()..addListener(_onScroll);
  87. WidgetsBinding.instance.addPostFrameCallback((_) => _load());
  88. }
  89. @override
  90. void dispose() {
  91. _aeNoCtrl.dispose(); _startDateCtrl.dispose(); _endDateCtrl.dispose();
  92. _scrollCtrl.dispose();
  93. super.dispose();
  94. }
  95. void _onScroll() {
  96. if (_scrollCtrl.position.pixels >= _scrollCtrl.position.maxScrollExtent - 200) {
  97. _load(append: true);
  98. }
  99. }
  100. Future<void> _load({bool append = false}) async {
  101. if (_loading) return;
  102. setState(() => _loading = true);
  103. try {
  104. final api = ref.read(expenseApiProvider);
  105. final result = await api.getImportableExpenseApplies(
  106. aeNo: _aeNoCtrl.text.trim(),
  107. startDate: _startDateCtrl.text,
  108. endDate: _endDateCtrl.text,
  109. page: append ? _page : 1,
  110. );
  111. if (!mounted) return;
  112. final list = (result['list'] as List<dynamic>?)
  113. ?.map((e) => ImportableItem.fromJson(e as Map<String, dynamic>))
  114. .toList() ?? [];
  115. setState(() {
  116. if (append) { _items.addAll(list); _page++; } else { _items = list; _page = 2; }
  117. _loading = false;
  118. _hasMore = list.length >= 20;
  119. });
  120. } catch (_) {
  121. if (mounted) setState(() => _loading = false);
  122. }
  123. }
  124. void _search() {
  125. _validateDates();
  126. if (_startDateCtrl.text.isNotEmpty && _endDateCtrl.text.isNotEmpty && _startDateCtrl.text.compareTo(_endDateCtrl.text) > 0) return;
  127. _page = 1; _load();
  128. }
  129. Future<void> _refresh() async {
  130. _page = 1;
  131. await _load();
  132. }
  133. void _toggleItem(int idx) {
  134. setState(() => _items[idx].selected = !_items[idx].selected);
  135. }
  136. void _toggleGroup(String aeNo) {
  137. setState(() {
  138. final items = _items.where((e) => e.aeNo == aeNo).toList();
  139. final allSelected = items.every((e) => e.selected);
  140. final newVal = !allSelected;
  141. for (final e in items) { e.selected = newVal; }
  142. });
  143. }
  144. bool _isGroupAllSelected(String aeNo) {
  145. final items = _items.where((e) => e.aeNo == aeNo);
  146. if (items.isEmpty) return false;
  147. return items.every((e) => e.selected);
  148. }
  149. bool _isGroupAnySelected(String aeNo) {
  150. return _items.any((e) => e.aeNo == aeNo && e.selected);
  151. }
  152. void _confirmImport() {
  153. final l10n = AppLocalizations.of(context);
  154. final selected = _items.where((e) => e.selected).toList();
  155. if (selected.isEmpty) {
  156. TDToast.showText(l10n.get('pleaseSelect'), context: context);
  157. return;
  158. }
  159. Navigator.of(context).pop(selected);
  160. }
  161. void _pickDate(TextEditingController ctrl) {
  162. final l10n = AppLocalizations.of(context);
  163. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  164. final now = DateTime.now();
  165. TDPicker.showDatePicker(
  166. context,
  167. title: l10n.get('selectDate'),
  168. backgroundColor: colors.bgCard,
  169. useYear: true, useMonth: true, useDay: true,
  170. useHour: false, useMinute: false, useSecond: false, useWeekDay: false,
  171. dateStart: const [2020, 1, 1],
  172. dateEnd: [now.year + 1, 12, 31],
  173. initialDate: [now.year, now.month, now.day],
  174. onConfirm: (selected) {
  175. final d = '${selected['year']}-${(selected['month'] ?? '').toString().padLeft(2, '0')}-${(selected['day'] ?? '').toString().padLeft(2, '0')}';
  176. ctrl.text = d;
  177. setState(() {});
  178. _validateDates();
  179. },
  180. );
  181. }
  182. void _validateDates() {
  183. if (_startDateCtrl.text.isNotEmpty && _endDateCtrl.text.isNotEmpty) {
  184. if (_startDateCtrl.text.compareTo(_endDateCtrl.text) > 0) {
  185. TDToast.showText(AppLocalizations.of(context).get('filterDateStartAfterEnd'), context: context);
  186. }
  187. }
  188. }
  189. Widget _buildSearchBar(AppLocalizations l10n, AppColorsExtension colors) {
  190. return Container(
  191. color: colors.bgCard,
  192. padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
  193. child: Column(
  194. mainAxisSize: MainAxisSize.min,
  195. children: [
  196. _inputRow(
  197. label: l10n.get('expenseApplyNo'),
  198. controller: _aeNoCtrl,
  199. hint: l10n.get('expenseApplyNo'),
  200. onSubmitted: (_) => _search(),
  201. ),
  202. const SizedBox(height: 8),
  203. Row(children: [
  204. Expanded(
  205. child: _pickerRow(
  206. label: l10n.get('filterStartDate'), value: _startDateCtrl.text,
  207. hint: l10n.get('filterStartDate'), hasValue: _startDateCtrl.text.isNotEmpty,
  208. onClear: () { _startDateCtrl.clear(); setState(() {}); },
  209. onTap: () => _pickDate(_startDateCtrl),
  210. ),
  211. ),
  212. const SizedBox(width: 8),
  213. Expanded(
  214. child: _pickerRow(
  215. label: l10n.get('filterEndDate'), value: _endDateCtrl.text,
  216. hint: l10n.get('filterEndDate'), hasValue: _endDateCtrl.text.isNotEmpty,
  217. onClear: () { _endDateCtrl.clear(); setState(() {}); },
  218. onTap: () => _pickDate(_endDateCtrl),
  219. ),
  220. ),
  221. const SizedBox(width: 8),
  222. GestureDetector(
  223. onTap: _search,
  224. child: Container(
  225. width: 40, height: 40,
  226. decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(20)),
  227. child: const Icon(Icons.search, color: Colors.white, size: 20),
  228. ),
  229. ),
  230. ]),
  231. ],
  232. ),
  233. );
  234. }
  235. Widget _inputRow({
  236. required String label, required TextEditingController controller,
  237. required String hint, ValueChanged<String>? onSubmitted,
  238. }) {
  239. final tdTheme = TDTheme.of(context);
  240. return Container(
  241. padding: const EdgeInsets.only(left: 16, right: 10, top: 10, bottom: 10),
  242. decoration: BoxDecoration(
  243. color: tdTheme.bgColorContainer,
  244. borderRadius: BorderRadius.circular(tdTheme.radiusDefault),
  245. border: Border.all(color: tdTheme.componentStrokeColor),
  246. ),
  247. child: Row(children: [
  248. TDText(label, maxLines: 1, overflow: TextOverflow.visible, font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400, style: const TextStyle(letterSpacing: 0)),
  249. const SizedBox(width: 12),
  250. Expanded(
  251. child: TextField(
  252. controller: controller,
  253. textAlign: TextAlign.end,
  254. style: TextStyle(fontSize: 16, color: Theme.of(context).extension<AppColorsExtension>()!.textPrimary),
  255. decoration: InputDecoration(
  256. hintText: hint,
  257. hintStyle: TextStyle(fontSize: 16, color: Theme.of(context).extension<AppColorsExtension>()!.textPlaceholder),
  258. border: InputBorder.none, isDense: true, contentPadding: EdgeInsets.zero,
  259. ),
  260. onSubmitted: onSubmitted,
  261. onChanged: (_) => setState(() {}),
  262. ),
  263. ),
  264. if (controller.text.isNotEmpty) ...[
  265. const SizedBox(width: 4),
  266. GestureDetector(
  267. onTap: () { controller.clear(); setState(() {}); },
  268. child: Icon(Icons.close, size: 18, color: tdTheme.textColorPlaceholder),
  269. ),
  270. ],
  271. ]),
  272. );
  273. }
  274. Widget _pickerRow({
  275. required String label,
  276. required String value,
  277. required String hint,
  278. required bool hasValue,
  279. VoidCallback? onClear,
  280. VoidCallback? onTap,
  281. Widget? child,
  282. }) {
  283. final tdTheme = TDTheme.of(context);
  284. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  285. return GestureDetector(
  286. onTap: onTap,
  287. child: Container(
  288. padding: const EdgeInsets.only(left: 16, right: 10, top: 12, bottom: 12),
  289. decoration: BoxDecoration(
  290. color: colors.bgSecondaryContainer,
  291. borderRadius: BorderRadius.circular(tdTheme.radiusDefault),
  292. border: Border.all(color: tdTheme.componentStrokeColor),
  293. ),
  294. child: child ?? Row(children: [
  295. TDText(label, maxLines: 1, overflow: TextOverflow.visible, font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400, style: const TextStyle(letterSpacing: 0)),
  296. const SizedBox(width: 12),
  297. Expanded(child: Row(mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.max, children: [
  298. Flexible(child: TDText(value.isNotEmpty ? value : hint, maxLines: 1, overflow: TextOverflow.ellipsis, font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400,
  299. textColor: value.isNotEmpty ? tdTheme.textColorPrimary : tdTheme.textColorPlaceholder, textAlign: TextAlign.end)),
  300. const SizedBox(width: 4),
  301. if (hasValue)
  302. GestureDetector(onTap: onClear, child: Icon(Icons.close, size: 18, color: tdTheme.textColorPlaceholder))
  303. else
  304. Icon(Icons.chevron_right, size: 18, color: tdTheme.textColorPlaceholder),
  305. ])),
  306. ]),
  307. ),
  308. );
  309. }
  310. Widget _buildSkeleton(AppColorsExtension colors) {
  311. return ListView.builder(
  312. itemCount: 5,
  313. itemBuilder: (ctx, i) => Container(
  314. margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
  315. padding: const EdgeInsets.all(16),
  316. decoration: BoxDecoration(color: colors.bgCard, borderRadius: BorderRadius.circular(12)),
  317. child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
  318. Row(children: [
  319. _skeletonBox(22, 22, colors),
  320. const SizedBox(width: 8),
  321. _skeletonBox(160, 16, colors),
  322. const Spacer(),
  323. _skeletonBox(80, 14, colors),
  324. ]),
  325. const SizedBox(height: 8),
  326. _skeletonBox(200, 12, colors),
  327. const SizedBox(height: 12),
  328. Divider(height: 1, color: colors.border),
  329. const SizedBox(height: 8),
  330. Row(children: [
  331. _skeletonBox(18, 18, colors),
  332. const SizedBox(width: 8),
  333. _skeletonBox(30, 12, colors),
  334. const SizedBox(width: 8),
  335. Expanded(child: _skeletonBox(double.infinity, 40, colors)),
  336. ]),
  337. ]),
  338. ),
  339. );
  340. }
  341. Widget _skeletonBox(double w, double h, AppColorsExtension colors) {
  342. return Container(
  343. width: w, height: h,
  344. decoration: BoxDecoration(
  345. color: colors.bgPage,
  346. borderRadius: BorderRadius.circular(6),
  347. ),
  348. );
  349. }
  350. Widget _buildHeaderCheckbox(String aeNo, AppColorsExtension colors) {
  351. final allSel = _isGroupAllSelected(aeNo);
  352. final anySel = _isGroupAnySelected(aeNo);
  353. IconData icon;
  354. Color iconColor;
  355. if (allSel) {
  356. icon = Icons.check_box;
  357. iconColor = colors.primary;
  358. } else if (anySel) {
  359. icon = Icons.indeterminate_check_box;
  360. iconColor = colors.primary;
  361. } else {
  362. icon = Icons.check_box_outline_blank;
  363. iconColor = colors.textPlaceholder;
  364. }
  365. return GestureDetector(
  366. onTap: () => _toggleGroup(aeNo),
  367. child: Icon(icon, size: 22, color: iconColor),
  368. );
  369. }
  370. Widget _buildItemCheckbox(int idx, AppColorsExtension colors) {
  371. final item = _items[idx];
  372. return GestureDetector(
  373. onTap: () => _toggleItem(idx),
  374. child: Icon(
  375. item.selected ? Icons.check_box : Icons.check_box_outline_blank,
  376. size: 18,
  377. color: item.selected ? colors.primary : colors.textPlaceholder,
  378. ),
  379. );
  380. }
  381. @override
  382. Widget build(BuildContext context) {
  383. final l10n = AppLocalizations.of(context);
  384. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  385. ref.read(navBarConfigProvider.notifier).update(
  386. NavBarConfig(title: l10n.get('importExpenseApply'), showBack: true),
  387. );
  388. final grouped = <String, List<ImportableItem>>{};
  389. for (final item in _items) {
  390. grouped.putIfAbsent(item.aeNo, () => []).add(item);
  391. }
  392. return Scaffold(
  393. backgroundColor: colors.bgPage,
  394. body: Column(children: [
  395. _buildSearchBar(l10n, colors),
  396. Expanded(
  397. child: RefreshIndicator(
  398. onRefresh: _refresh,
  399. child: _loading && _items.isEmpty
  400. ? _buildSkeleton(colors)
  401. : _items.isEmpty
  402. ? ListView(children: [SizedBox(height: MediaQuery.of(context).size.height * 0.3, child: Center(child: Text(l10n.get('noData'), style: TextStyle(fontSize: 14, color: colors.textPlaceholder))))])
  403. : ListView.builder(
  404. controller: _scrollCtrl,
  405. itemCount: grouped.length + (_hasMore ? 1 : 0) + (_items.isNotEmpty && !_hasMore ? 1 : 0),
  406. itemBuilder: (ctx, i) {
  407. if (i >= grouped.length && _hasMore) {
  408. if (!_loading) { WidgetsBinding.instance.addPostFrameCallback((_) => _load(append: true)); }
  409. return const Center(child: Padding(padding: EdgeInsets.all(16), child: TDLoading(size: TDLoadingSize.medium, icon: TDLoadingIcon.activity)));
  410. }
  411. if (i >= grouped.length) {
  412. return Center(child: Padding(padding: const EdgeInsets.all(16), child: Text(l10n.get('noMoreData'), style: TextStyle(fontSize: 13, color: colors.textPlaceholder))));
  413. }
  414. final aeNo = grouped.keys.elementAt(i);
  415. final items = grouped[aeNo]!;
  416. return Container(
  417. margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
  418. decoration: BoxDecoration(
  419. color: colors.bgCard,
  420. borderRadius: BorderRadius.circular(12),
  421. ),
  422. child: Column(
  423. crossAxisAlignment: CrossAxisAlignment.start,
  424. children: [
  425. // Header row: 点击整行切换全选
  426. GestureDetector(
  427. behavior: HitTestBehavior.opaque,
  428. onTap: () => _toggleGroup(aeNo),
  429. child: Padding(
  430. padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
  431. child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
  432. Row(children: [
  433. _buildHeaderCheckbox(aeNo, colors),
  434. const SizedBox(width: 8),
  435. Expanded(child: Text(aeNo, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.textPrimary))),
  436. Text(items.first.aeDd, style: TextStyle(fontSize: 13, color: colors.textSecondary)),
  437. ]),
  438. if (items.first.reason.isNotEmpty)
  439. Padding(
  440. padding: const EdgeInsets.only(top: 4, left: 30),
  441. child: Text(items.first.reason, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 13, color: colors.textSecondary)),
  442. ),
  443. ]),
  444. ),
  445. ),
  446. Divider(height: 1, color: colors.border),
  447. // Detail rows: 点击整行切换勾选
  448. ...items.asMap().entries.map((entry) {
  449. final idx = _items.indexOf(entry.value);
  450. final d = entry.value;
  451. return GestureDetector(
  452. behavior: HitTestBehavior.opaque,
  453. onTap: () => _toggleItem(idx),
  454. child: Padding(
  455. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
  456. child: Row(children: [
  457. SizedBox(width: 30, child: _buildItemCheckbox(idx, colors)),
  458. const SizedBox(width: 4),
  459. Container(width: 24, alignment: Alignment.center, child: Text('#${d.itm}', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.textSecondary))),
  460. const SizedBox(width: 8),
  461. Expanded(
  462. child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
  463. Text('${d.typeName.isNotEmpty ? '${d.typeNo}/${d.typeName}' : d.typeNo} ${d.accName}', style: TextStyle(fontSize: 14, color: colors.textPrimary)),
  464. const SizedBox(height: 3),
  465. if (d.sqMan.isNotEmpty)
  466. Padding(
  467. padding: const EdgeInsets.only(bottom: 2),
  468. child: Text('${l10n.get('applicant')}: ${d.sqName.isNotEmpty ? '${d.sqMan}/${d.sqName}' : d.sqMan}', style: TextStyle(fontSize: 13, color: colors.textSecondary)),
  469. ),
  470. Padding(
  471. padding: const EdgeInsets.only(bottom: 2),
  472. child: Text('${l10n.get('acctSubject')}: ${d.accNo}/${d.accName}', style: TextStyle(fontSize: 13, color: colors.textSecondary)),
  473. ),
  474. if (d.depName.isNotEmpty)
  475. Padding(
  476. padding: const EdgeInsets.only(bottom: 2),
  477. child: Text('${l10n.get('dept')}: ${d.dep}/${d.depName}', style: TextStyle(fontSize: 13, color: colors.textSecondary)),
  478. ),
  479. if (d.objName.isNotEmpty)
  480. Padding(
  481. padding: const EdgeInsets.only(bottom: 2),
  482. child: Text('${l10n.get('project')}: ${d.objNo}/${d.objName}', style: TextStyle(fontSize: 13, color: colors.textSecondary)),
  483. ),
  484. if (d.startDd.isNotEmpty || d.endDd.isNotEmpty)
  485. Text('${l10n.get('date')}: ${d.startDd} ~ ${d.endDd}', style: TextStyle(fontSize: 13, color: colors.textSecondary)),
  486. ]),
  487. ),
  488. Text('¥${d.amtnYj.toStringAsFixed(2)}', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.amountPrimary)),
  489. ]),
  490. ),
  491. );
  492. }),
  493. if (items.length > 1)
  494. Padding(
  495. padding: const EdgeInsets.fromLTRB(0, 4, 16, 12),
  496. child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
  497. Text('${l10n.get('total')} ${items.length} ${l10n.get('unitItem')}', style: TextStyle(fontSize: 12, color: colors.textSecondary)),
  498. ]),
  499. )
  500. else
  501. const SizedBox(height: 8),
  502. ],
  503. ),
  504. );
  505. },
  506. ),
  507. ),
  508. ),
  509. ]),
  510. bottomNavigationBar: SafeArea(
  511. child: Padding(
  512. padding: const EdgeInsets.all(12),
  513. child: TDButton(
  514. text: l10n.get('confirmImport'),
  515. size: TDButtonSize.large,
  516. theme: TDButtonTheme.primary,
  517. onTap: _confirmImport,
  518. ),
  519. ),
  520. ),
  521. );
  522. }
  523. }