| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611 |
- import 'package:flutter/material.dart';
- import 'package:flutter_riverpod/flutter_riverpod.dart';
- import 'package:go_router/go_router.dart';
- import 'package:tdesign_flutter/tdesign_flutter.dart';
- import 'package:easy_refresh/easy_refresh.dart';
- import '../../shared/widgets/nav_bar_config.dart';
- import '../../core/i18n/app_localizations.dart';
- import '../../core/theme/app_colors_extension.dart';
- import '../../shared/widgets/empty_state.dart';
- import '../../shared/widgets/skeleton_list_card.dart';
- import '../../shared/widgets/list_footer.dart';
- import 'expense_api.dart';
- /// 可导入的费用申请明细项
- class ImportableItem {
- final String aeNo;
- final String aeDd;
- final String reason;
- final double headAmtnYj;
- final int itm;
- final String sqMan;
- final String sqName;
- final String typeNo;
- final String typeName;
- final double amtnYj;
- final String accNo;
- final String accName;
- final String dep;
- final String depName;
- final String objNo;
- final String objName;
- final String startDd;
- final String endDd;
- final String priority;
- final String rem;
- bool selected = false;
- ImportableItem({
- required this.aeNo, required this.aeDd, required this.reason,
- required this.headAmtnYj, required this.itm, required this.sqMan, required this.sqName,
- required this.typeNo, required this.typeName, required this.amtnYj,
- required this.accNo, required this.accName, required this.dep,
- required this.depName, required this.objNo, required this.objName,
- required this.startDd, required this.endDd, required this.priority, required this.rem,
- });
- factory ImportableItem.fromJson(Map<String, dynamic> json) => ImportableItem(
- aeNo: json['aeNo'] as String? ?? '',
- aeDd: _fmtDate(json['aeDd'] as String?),
- reason: json['reason'] as String? ?? '',
- headAmtnYj: (json['headAmtnYj'] as num?)?.toDouble() ?? 0,
- itm: json['itm'] as int? ?? 0,
- sqMan: json['sqMan'] as String? ?? '',
- sqName: json['sqName'] as String? ?? '',
- typeNo: json['typeNo'] as String? ?? '',
- typeName: json['typeName'] as String? ?? '',
- amtnYj: (json['amtnYj'] as num?)?.toDouble() ?? 0,
- accNo: json['accNo'] as String? ?? '',
- accName: json['accName'] as String? ?? '',
- dep: json['dep'] as String? ?? '',
- depName: json['depName'] as String? ?? '',
- objNo: json['objNo'] as String? ?? '',
- objName: json['objName'] as String? ?? '',
- startDd: _fmtDate(json['startDd'] as String?),
- endDd: _fmtDate(json['endDd'] as String?),
- priority: json['priority'] as String? ?? '1',
- rem: json['rem'] as String? ?? '',
- );
- static String _fmtDate(String? raw) {
- if (raw == null || raw.isEmpty) return '';
- try {
- final d = DateTime.parse(raw);
- return '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
- } catch (_) {
- return raw;
- }
- }
- }
- class ExpenseApplyImportPage extends ConsumerStatefulWidget {
- const ExpenseApplyImportPage({super.key});
- @override
- ConsumerState<ExpenseApplyImportPage> createState() => _ExpenseApplyImportPageState();
- }
- class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
- with WidgetsBindingObserver {
- final _aeNoCtrl = TextEditingController();
- final _startDateCtrl = TextEditingController();
- final _endDateCtrl = TextEditingController();
- List<ImportableItem> _items = [];
- final Map<String, bool> _expandedGroups = {};
- bool _loading = false;
- bool _hasMore = true;
- int _page = 1;
- late final ScrollController _scrollCtrl;
- late final EasyRefreshController _refreshCtrl;
- bool _sortDesc = true;
- @override
- void initState() {
- super.initState();
- final now = DateTime.now();
- _startDateCtrl.text = '${now.year}-${now.month.toString().padLeft(2, '0')}-01';
- _endDateCtrl.text = '${now.year}-${now.month.toString().padLeft(2, '0')}-${DateTime(now.year, now.month + 1, 0).day.toString().padLeft(2, '0')}';
- _scrollCtrl = ScrollController()..addListener(_onScroll);
- _refreshCtrl = EasyRefreshController();
- WidgetsBinding.instance.addObserver(this);
- WidgetsBinding.instance.addPostFrameCallback((_) => _load());
- }
- @override
- void dispose() {
- WidgetsBinding.instance.removeObserver(this);
- _aeNoCtrl.dispose(); _startDateCtrl.dispose(); _endDateCtrl.dispose();
- _scrollCtrl.dispose();
- _refreshCtrl.dispose();
- super.dispose();
- }
- @override
- void didChangeAppLifecycleState(AppLifecycleState state) {
- if (state == AppLifecycleState.resumed) {
- _aeNoCtrl.clear();
- final now = DateTime.now();
- _startDateCtrl.text = '${now.year}-${now.month.toString().padLeft(2, '0')}-01';
- _endDateCtrl.text = '${now.year}-${now.month.toString().padLeft(2, '0')}-${DateTime(now.year, now.month + 1, 0).day.toString().padLeft(2, '0')}';
- _items = []; _page = 1; _hasMore = true; _generation++;
- _load();
- }
- }
- void _onScroll() {
- if (!_loading && _hasMore && _scrollCtrl.position.pixels >= _scrollCtrl.position.maxScrollExtent - 200) {
- _load(append: true);
- }
- }
- int _generation = 0;
- Future<void> _load({bool append = false}) async {
- if (_loading) return;
- final gen = ++_generation;
- setState(() => _loading = true);
- try {
- final api = ref.read(expenseApiProvider);
- final result = await api.getImportableExpenseApplies(
- keyword: _aeNoCtrl.text.trim(),
- startDate: _startDateCtrl.text,
- endDate: _endDateCtrl.text,
- page: append ? _page : 1,
- sortDir: _sortDesc ? 'DESC' : 'ASC',
- );
- if (!mounted) return;
- if (gen != _generation) return;
- final list = (result['list'] as List<dynamic>?)
- ?.map((e) => ImportableItem.fromJson(e as Map<String, dynamic>))
- .toList() ?? [];
- setState(() {
- if (append) { _items.addAll(list); _page++; } else { _items = list; _page = 2; }
- _loading = false;
- _hasMore = list.length >= 20;
- });
- } catch (_) {
- if (mounted) setState(() => _loading = false);
- }
- }
- void _search() {
- FocusScope.of(context).unfocus();
- if (_startDateCtrl.text.isNotEmpty && _endDateCtrl.text.isNotEmpty && _startDateCtrl.text.compareTo(_endDateCtrl.text) > 0) {
- TDToast.showText(AppLocalizations.of(context).get('filterDateStartAfterEnd'), context: context);
- return;
- }
- _refreshCtrl.callRefresh();
- }
- Future<void> _refresh() async {
- FocusScope.of(context).unfocus();
- _page = 1;
- _loading = false;
- await _load();
- }
- void _toggleItem(int idx) {
- setState(() => _items[idx].selected = !_items[idx].selected);
- }
- void _toggleGroup(String aeNo) {
- setState(() {
- final items = _items.where((e) => e.aeNo == aeNo).toList();
- final allSelected = items.every((e) => e.selected);
- final newVal = !allSelected;
- for (final e in items) { e.selected = newVal; }
- });
- }
- bool _isGroupAllSelected(String aeNo) {
- final items = _items.where((e) => e.aeNo == aeNo);
- if (items.isEmpty) return false;
- return items.every((e) => e.selected);
- }
- bool _isGroupAnySelected(String aeNo) {
- return _items.any((e) => e.aeNo == aeNo && e.selected);
- }
- void _confirmImport() {
- final l10n = AppLocalizations.of(context);
- final selected = _items.where((e) => e.selected).toList();
- if (selected.isEmpty) {
- TDToast.showText(l10n.get('pleaseSelect'), context: context);
- return;
- }
- Navigator.of(context).pop(selected);
- }
- void _pickDate(TextEditingController ctrl) {
- FocusScope.of(context).unfocus();
- final l10n = AppLocalizations.of(context);
- final colors = Theme.of(context).extension<AppColorsExtension>()!;
- final now = DateTime.now();
- TDPicker.showDatePicker(
- context,
- title: l10n.get('selectDate'),
- backgroundColor: colors.bgCard,
- useYear: true, useMonth: true, useDay: true,
- useHour: false, useMinute: false, useSecond: false, useWeekDay: false,
- dateStart: const [2020, 1, 1],
- dateEnd: [now.year + 1, 12, 31],
- initialDate: [now.year, now.month, now.day],
- onConfirm: (selected) {
- final d = '${selected['year']}-${selected['month'].toString().padLeft(2, '0')}-${selected['day'].toString().padLeft(2, '0')}';
- if (_validateDateRange(ctrl, d)) {
- ctrl.text = d;
- setState(() {});
- }
- Navigator.of(context).pop();
- },
- );
- }
- bool _validateDateRange(TextEditingController changed, String newValue) {
- final start = changed == _startDateCtrl ? newValue : _startDateCtrl.text;
- final end = changed == _endDateCtrl ? newValue : _endDateCtrl.text;
- if (start.isNotEmpty && end.isNotEmpty && start.compareTo(end) > 0) {
- TDToast.showText(AppLocalizations.of(context).get('filterDateStartAfterEnd'), context: context);
- return false;
- }
- return true;
- }
- Widget _buildSearchBar(AppLocalizations l10n, AppColorsExtension colors) {
- final tdTheme = TDTheme.of(context);
- return Container(
- decoration: BoxDecoration(
- color: colors.bgCard,
- border: Border(bottom: BorderSide(color: tdTheme.componentStrokeColor)),
- ),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Padding(
- padding: const EdgeInsets.fromLTRB(12, 8, 12, 0),
- child: Row(children: [
- Expanded(
- child: GestureDetector(
- onTap: () => _pickDate(_startDateCtrl),
- child: _dateChip(_startDateCtrl, l10n.get('filterStartDate'), tdTheme, colors),
- ),
- ),
- const SizedBox(width: 8),
- Text('—', style: TextStyle(fontSize: 14, color: colors.textSecondary)),
- const SizedBox(width: 8),
- Expanded(
- child: GestureDetector(
- onTap: () => _pickDate(_endDateCtrl),
- child: _dateChip(_endDateCtrl, l10n.get('filterEndDate'), tdTheme, colors),
- ),
- ),
- ]),
- ),
- const SizedBox(height: 8),
- Padding(
- padding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
- child: Row(children: [
- Expanded(
- child: Container(
- height: 40,
- padding: const EdgeInsets.symmetric(horizontal: 16),
- decoration: BoxDecoration(
- color: colors.bgSecondaryContainer,
- borderRadius: BorderRadius.circular(20),
- border: Border.all(color: tdTheme.componentStrokeColor),
- ),
- child: Row(children: [
- Expanded(
- child: TextField(
- controller: _aeNoCtrl,
- style: TextStyle(fontSize: 14, color: colors.textPrimary),
- decoration: InputDecoration(
- hintText: l10n.get('searchImportHint'),
- hintStyle: TextStyle(fontSize: 14, color: colors.textSecondary),
- border: InputBorder.none,
- isCollapsed: true,
- ),
- onChanged: (_) => setState(() {}),
- ),
- ),
- if (_aeNoCtrl.text.isNotEmpty)
- GestureDetector(
- onTap: () { _aeNoCtrl.clear(); setState(() {}); },
- child: Icon(Icons.close, size: 18, color: colors.textSecondary),
- ),
- ]),
- ),
- ),
- const SizedBox(width: 8),
- GestureDetector(
- onTap: () { setState(() => _sortDesc = !_sortDesc); _search(); },
- child: Container(
- width: 40, height: 40,
- decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(20)),
- child: Center(child: Icon(_sortDesc ? Icons.arrow_downward : Icons.arrow_upward, color: Colors.white, size: 20)),
- ),
- ),
- const SizedBox(width: 8),
- GestureDetector(
- onTap: _search,
- child: Container(
- width: 40, height: 40,
- decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(20)),
- child: const Icon(Icons.search, color: Colors.white, size: 22),
- ),
- ),
- ]),
- ),
- ],
- ),
- );
- }
- Widget _dateChip(TextEditingController ctrl, String hint, TDThemeData tdTheme, AppColorsExtension colors) {
- final text = ctrl.text;
- return Container(
- height: 40,
- padding: const EdgeInsets.symmetric(horizontal: 12),
- decoration: BoxDecoration(
- color: colors.bgSecondaryContainer,
- borderRadius: BorderRadius.circular(20),
- border: Border.all(color: tdTheme.componentStrokeColor),
- ),
- child: Row(children: [
- Icon(Icons.calendar_today, size: 16, color: colors.textSecondary),
- const SizedBox(width: 6),
- Expanded(
- child: Text(
- text.isNotEmpty ? text : hint,
- maxLines: 1, overflow: TextOverflow.ellipsis,
- style: TextStyle(fontSize: 14, color: text.isNotEmpty ? colors.textPrimary : colors.textSecondary),
- ),
- ),
- if (text.isNotEmpty)
- GestureDetector(
- onTap: () { ctrl.clear(); setState(() {}); },
- child: Icon(Icons.close, size: 18, color: colors.textSecondary),
- ),
- ]),
- );
- }
- Widget _buildListContent(AppLocalizations l10n, AppColorsExtension colors, Map<String, List<ImportableItem>> grouped) {
- if (_loading && _items.isEmpty) {
- return EasyRefresh(
- header: TDRefreshHeader(),
- controller: _refreshCtrl,
- onRefresh: _refresh,
- child: SkeletonLoadingList(
- cardBuilder: () => const SkeletonImportCard(),
- ),
- );
- }
- if (_items.isEmpty) {
- return EasyRefresh(
- header: TDRefreshHeader(),
- controller: _refreshCtrl,
- onRefresh: _refresh,
- child: ListView(
- children: [const SizedBox(height: 120), EmptyState(message: l10n.get('noData'))],
- ),
- );
- }
- return EasyRefresh(
- header: TDRefreshHeader(),
- controller: _refreshCtrl,
- onRefresh: _refresh,
- child: ListView.builder(
- controller: _scrollCtrl,
- padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
- itemCount: grouped.length + 1,
- itemBuilder: (_, i) {
- if (i == grouped.length) {
- return ListFooter(itemCount: _items.length, hasMore: _hasMore);
- }
- final aeNo = grouped.keys.elementAt(i);
- return _buildGroupCard(aeNo, grouped[aeNo]!, l10n, colors);
- },
- ),
- );
- }
- Widget _buildGroupCard(String aeNo, List<ImportableItem> items, AppLocalizations l10n, AppColorsExtension colors) {
- return Padding(
- padding: const EdgeInsets.only(bottom: 16),
- child: Container(
- decoration: BoxDecoration(
- color: colors.bgCard,
- borderRadius: BorderRadius.circular(12),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- GestureDetector(
- behavior: HitTestBehavior.opaque,
- onTap: () => _toggleGroup(aeNo),
- child: Padding(
- padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
- child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
- Row(children: [
- _buildHeaderCheckbox(aeNo, colors),
- const SizedBox(width: 8),
- Expanded(child: Text(aeNo, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.textPrimary))),
- Column(crossAxisAlignment: CrossAxisAlignment.end, children: [
- Text(items.first.aeDd, style: TextStyle(fontSize: 13, color: colors.textSecondary)),
- const SizedBox(height: 2),
- _priorityChip(items.first.priority, l10n, colors),
- ]),
- ]),
- if (items.first.reason.isNotEmpty)
- Padding(
- padding: const EdgeInsets.only(top: 4, left: 30),
- child: Text(items.first.reason, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 13, color: colors.textSecondary)),
- ),
- ]),
- ),
- ),
- Divider(height: 1, color: colors.border),
- if (items.length <= 3)
- ...items.asMap().entries.map((entry) => _buildDetailRow(entry.value, _items.indexOf(entry.value), l10n, colors))
- else ...[
- ...items.take(3).toList().asMap().entries.map((entry) => _buildDetailRow(entry.value, _items.indexOf(entry.value), l10n, colors)),
- GestureDetector(
- onTap: () => setState(() => _expandedGroups[aeNo] = !(_expandedGroups[aeNo] ?? false)),
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.end,
- children: [
- Text(
- _expandedGroups[aeNo] == true
- ? '${l10n.getString('collapseRemaining', args: {'count': (items.length - 3).toString()})} ▲'
- : '${l10n.getString('expandRemaining', args: {'count': (items.length - 3).toString()})} ▼',
- style: TextStyle(fontSize: 13, color: colors.primary),
- ),
- ],
- ),
- ),
- ),
- if (_expandedGroups[aeNo] == true)
- ...items.skip(3).toList().asMap().entries.map((entry) => _buildDetailRow(entry.value, _items.indexOf(entry.value), l10n, colors)),
- ],
- if (items.length > 1)
- Padding(
- padding: const EdgeInsets.fromLTRB(0, 4, 16, 12),
- child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
- Text('${l10n.get('total')} ${items.length} ${l10n.get('unitItem')}', style: TextStyle(fontSize: 12, color: colors.textSecondary)),
- ]),
- )
- else
- const SizedBox(height: 8),
- ],
- ),
- ),
- );
- }
- Widget _buildDetailRow(ImportableItem d, int idx, AppLocalizations l10n, AppColorsExtension colors) {
- return GestureDetector(
- behavior: HitTestBehavior.opaque,
- onTap: () => _toggleItem(idx),
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
- child: Row(children: [
- SizedBox(width: 30, child: _buildItemCheckbox(idx, colors)),
- const SizedBox(width: 4),
- Container(width: 24, alignment: Alignment.center, child: Text('#${d.itm}', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.textSecondary))),
- const SizedBox(width: 8),
- Expanded(
- child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
- Text('${d.typeName.isNotEmpty ? '${d.typeNo}/${d.typeName}' : d.typeNo} ${d.accName}', style: TextStyle(fontSize: 14, color: colors.textPrimary)),
- if (d.rem.isNotEmpty)
- Padding(padding: const EdgeInsets.only(top: 2), child: Text(d.rem, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 13, color: colors.textSecondary))),
- const SizedBox(height: 3),
- if (d.sqMan.isNotEmpty)
- Padding(padding: const EdgeInsets.only(bottom: 2), child: Text('${l10n.get('applicant')}: ${d.sqName.isNotEmpty ? '${d.sqMan}/${d.sqName}' : d.sqMan}', style: TextStyle(fontSize: 13, color: colors.textSecondary))),
- Padding(padding: const EdgeInsets.only(bottom: 2), child: Text('${l10n.get('acctSubject')}: ${d.accNo}/${d.accName}', style: TextStyle(fontSize: 13, color: colors.textSecondary))),
- if (d.depName.isNotEmpty)
- Padding(padding: const EdgeInsets.only(bottom: 2), child: Text('${l10n.get('dept')}: ${d.dep}/${d.depName}', style: TextStyle(fontSize: 13, color: colors.textSecondary))),
- if (d.objName.isNotEmpty)
- Padding(padding: const EdgeInsets.only(bottom: 2), child: Text('${l10n.get('project')}: ${d.objNo}/${d.objName}', style: TextStyle(fontSize: 13, color: colors.textSecondary))),
- if (d.startDd.isNotEmpty || d.endDd.isNotEmpty)
- Text('${l10n.get('date')}: ${d.startDd} ~ ${d.endDd}', style: TextStyle(fontSize: 13, color: colors.textSecondary)),
- ]),
- ),
- Text('¥${d.amtnYj.toStringAsFixed(2)}', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.amountPrimary)),
- ]),
- ),
- );
- }
- Widget _buildHeaderCheckbox(String aeNo, AppColorsExtension colors) {
- final allSel = _isGroupAllSelected(aeNo);
- final anySel = _isGroupAnySelected(aeNo);
- IconData icon;
- Color iconColor;
- if (allSel) {
- icon = Icons.check_box;
- iconColor = colors.primary;
- } else if (anySel) {
- icon = Icons.indeterminate_check_box;
- iconColor = colors.primary;
- } else {
- icon = Icons.check_box_outline_blank;
- iconColor = colors.textPlaceholder;
- }
- return GestureDetector(
- onTap: () => _toggleGroup(aeNo),
- child: Icon(icon, size: 22, color: iconColor),
- );
- }
- Widget _buildItemCheckbox(int idx, AppColorsExtension colors) {
- final item = _items[idx];
- return GestureDetector(
- onTap: () => _toggleItem(idx),
- child: Icon(
- item.selected ? Icons.check_box : Icons.check_box_outline_blank,
- size: 18,
- color: item.selected ? colors.primary : colors.textPlaceholder,
- ),
- );
- }
- Widget _priorityChip(String priority, AppLocalizations l10n, AppColorsExtension colors) {
- final label = priority == '3' ? l10n.get('filterCritical') : priority == '2' ? l10n.get('urgent') : l10n.get('normal');
- final chipColor = priority == '3' ? colors.danger : priority == '2' ? colors.warning : colors.primary;
- return Container(
- padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
- decoration: BoxDecoration(
- color: chipColor.withValues(alpha: 0.1),
- borderRadius: BorderRadius.circular(4),
- border: Border.all(color: chipColor, width: 0.5),
- ),
- child: Text(label, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: chipColor)),
- );
- }
- @override
- Widget build(BuildContext context) {
- final l10n = AppLocalizations.of(context);
- final colors = Theme.of(context).extension<AppColorsExtension>()!;
- setNavBarTitle(context, ref, NavBarConfig(
- title: l10n.get('importExpenseApply'),
- showBack: true,
- onBack: () => context.pop(),
- ));
- final grouped = <String, List<ImportableItem>>{};
- for (final item in _items) {
- grouped.putIfAbsent(item.aeNo, () => []).add(item);
- }
- return Scaffold(
- backgroundColor: colors.bgPage,
- body: GestureDetector(
- behavior: HitTestBehavior.translucent,
- onTap: () => FocusScope.of(context).unfocus(),
- child: Column(children: [
- _buildSearchBar(l10n, colors),
- const SizedBox(height: 8),
- Expanded(
- child: _buildListContent(l10n, colors, grouped),
- ),
- ]),
- ),
- bottomNavigationBar: SafeArea(
- child: Padding(
- padding: const EdgeInsets.all(12),
- child: TDButton(
- text: l10n.get('confirmImport'),
- size: TDButtonSize.large,
- theme: TDButtonTheme.primary,
- onTap: _confirmImport,
- ),
- ),
- ),
- );
- }
- }
|