chengc 2 hari lalu
induk
melakukan
85b50df58f

+ 6 - 2
lib/features/expense/expense_apply_import_page.dart

@@ -123,6 +123,7 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
   @override
   void didChangeAppLifecycleState(AppLifecycleState state) {
     if (state == AppLifecycleState.resumed) {
+      FocusScope.of(context).unfocus();
       _aeNoCtrl.clear();
       final now = DateTime.now();
       _startDateCtrl.text = '${now.year}-${now.month.toString().padLeft(2, '0')}-01';
@@ -218,7 +219,7 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
   }
 
   void _pickDate(TextEditingController ctrl) {
-    FocusScope.of(context).unfocus();
+    FocusManager.instance.primaryFocus?.unfocus();
     final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final now = DateTime.now();
@@ -574,7 +575,10 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
     setNavBarTitle(context, ref, NavBarConfig(
       title: l10n.get('importExpenseApply'),
       showBack: true,
-      onBack: () => context.pop(),
+      onBack: () {
+        FocusManager.instance.primaryFocus?.unfocus();
+        context.pop();
+      },
     ));
 
     final grouped = <String, List<ImportableItem>>{};

+ 39 - 29
lib/features/expense/expense_create_page.dart

@@ -173,35 +173,38 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage>
 
   @override
   void didChangeAppLifecycleState(AppLifecycleState state) {
-    if (state == AppLifecycleState.resumed && _isPoppingToNative) {
-      _isPoppingToNative = false;
-      HostAppChannel.refresh();
-      // 重置表单数据
-      _purposeController.clear();
-      _remarkController.clear();
-      _selectedDeptId = '';
-      _selectedDeptName = '';
-      _attachmentController.clear();
-      _attachAvailable = false;
-      _addingDetail = false;
-      // 重置参考数据
-      _costTypes = [];
-      _projects = [];
-      _departments = [];
-      _customers = [];
-      _employees = [];
-      _currencies = [];
-      _refDataFuture = null;
-      _refDataLoading = true;
-      // 重置草稿和控制器状态
-      _draftHandled = false;
-      _draftFuture = widget.editId == null
-          ? ExpenseCreateController.hasDraft()
-          : Future.value(false);
-      ref.read(expenseCreateProvider(widget.editId).notifier).reset();
-      // 重新加载
-      _loadRefData();
-      _checkAttachHealth();
+    if (state == AppLifecycleState.resumed) {
+      FocusScope.of(context).unfocus();
+      if (_isPoppingToNative) {
+        _isPoppingToNative = false;
+        HostAppChannel.refresh();
+        // 重置表单数据
+        _purposeController.clear();
+        _remarkController.clear();
+        _selectedDeptId = '';
+        _selectedDeptName = '';
+        _attachmentController.clear();
+        _attachAvailable = false;
+        _addingDetail = false;
+        // 重置参考数据
+        _costTypes = [];
+        _projects = [];
+        _departments = [];
+        _customers = [];
+        _employees = [];
+        _currencies = [];
+        _refDataFuture = null;
+        _refDataLoading = true;
+        // 重置草稿和控制器状态
+        _draftHandled = false;
+        _draftFuture = widget.editId == null
+            ? ExpenseCreateController.hasDraft()
+            : Future.value(false);
+        ref.read(expenseCreateProvider(widget.editId).notifier).reset();
+        // 重新加载
+        _loadRefData();
+        _checkAttachHealth();
+      }
     }
   }
 
@@ -293,6 +296,7 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage>
   void _showDraftDialog() {
     final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    FocusManager.instance.primaryFocus?.unfocus();
     showDialog(
       context: context,
       barrierDismissible: false,
@@ -382,6 +386,7 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage>
     final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final labels = _departments.map((d) => '${d.dep}/${d.name}').toList();
+    FocusManager.instance.primaryFocus?.unfocus();
     TDPicker.showMultiPicker(
       context,
       title: l10n.get('expenseDept'),
@@ -1128,6 +1133,7 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage>
         aeDd: d.aeDd,
       );
     }
+    FocusManager.instance.primaryFocus?.unfocus();
     final result = await ExpenseDetailDialog.show(
       context,
       categories: _dialogCategories,
@@ -1202,6 +1208,7 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage>
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final codes = _currencies.map((c) => c.curId).toList();
     final labels = _currencies.map((c) => '${c.curId}/${c.name}').toList();
+    FocusManager.instance.primaryFocus?.unfocus();
     TDPicker.showMultiPicker(
       context,
       title: l10n.get('selectCurrency'),
@@ -1225,6 +1232,7 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage>
     String initialText = '',
   }) {
     FocusScope.of(context).unfocus();
+    FocusManager.instance.primaryFocus?.unfocus();
     final l10n = AppLocalizations.of(context);
     final c = TextEditingController(text: initialText);
     showGeneralDialog(
@@ -1321,6 +1329,7 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage>
   }
 
   void _forcePop() {
+    FocusManager.instance.primaryFocus?.unfocus();
     final router = GoRouter.of(context);
     if (router.canPop()) {
       router.pop();
@@ -1338,6 +1347,7 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage>
     VoidCallback onConfirm,
   ) {
     FocusScope.of(context).unfocus();
+    FocusManager.instance.primaryFocus?.unfocus();
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     showDialog(
       context: context,

+ 6 - 1
lib/features/expense/expense_list_page.dart

@@ -48,6 +48,7 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
   @override
   void didChangeAppLifecycleState(AppLifecycleState state) {
     if (state == AppLifecycleState.resumed) {
+      FocusScope.of(context).unfocus();
       _keywordCtrl.clear();
       final now = DateTime.now();
       _startDateCtrl.text = '${now.year}-${now.month.toString().padLeft(2, '0')}-01';
@@ -70,6 +71,7 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
   }
 
   void _pickDate(TextEditingController ctrl) {
+    FocusManager.instance.primaryFocus?.unfocus();
     final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final now = DateTime.now();
@@ -118,7 +120,10 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
     setNavBarTitle(context, ref, NavBarConfig(
       title: l10n.get('expenseList'),
       showBack: true,
-      onBack: () => SystemNavigator.pop(),
+      onBack: () {
+        FocusManager.instance.primaryFocus?.unfocus();
+        SystemNavigator.pop();
+      },
       showRight: true,
       rightWidget: GestureDetector(
         onTap: () => context.push('/expense/create'),

+ 380 - 152
lib/features/expense/widgets/expense_detail_dialog.dart

@@ -122,8 +122,7 @@ class ExpenseDetailDialog extends StatefulWidget {
   }
 
   @override
-  State<ExpenseDetailDialog> createState() =>
-      _ExpenseDetailDialogState();
+  State<ExpenseDetailDialog> createState() => _ExpenseDetailDialogState();
 }
 
 class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
@@ -142,8 +141,39 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
   EmployeeItem? _selEmployee;
   late final AttachmentPickerController _attachmentCtrl;
   final ScrollController _scrollCtrl = ScrollController();
+  final _remarkFocus = FocusNode();
+  final _bankNameFocus = FocusNode();
+  final _bankAccountNameFocus = FocusNode();
+  final _bankAccountFocus = FocusNode();
   bool _attachAvailable = false;
 
+  void _ensureVisible(FocusNode node) {
+    if (!node.hasFocus) return;
+    _doEnsureVisible(node, 0, -1);
+  }
+
+  void _doEnsureVisible(FocusNode node, int attempt, double lastInsets) {
+    if (attempt >= 15) return;
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      if (!mounted || !node.hasFocus || !_scrollCtrl.hasClients) return;
+      final insets = MediaQuery.of(context).viewInsets.bottom;
+      if (insets != lastInsets) {
+        _doEnsureVisible(node, attempt + 1, insets);
+        return;
+      }
+      Future.delayed(const Duration(milliseconds: 500), () {
+        if (!mounted || !node.hasFocus || !_scrollCtrl.hasClients) return;
+        final ctx = node.context;
+        if (ctx == null) return;
+        Scrollable.ensureVisible(
+          ctx,
+          alignment: 0.5,
+          duration: const Duration(milliseconds: 300),
+        );
+      });
+    });
+  }
+
   static const _taxOptions = [0.0, 0.06, 0.09, 0.13];
   static const _taxLabels = ['0%', '6%', '9%', '13%'];
 
@@ -159,17 +189,29 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
     super.initState();
     final d = widget.initialData;
     _cat = d != null
-        ? (_cats.any((c) => c.code == d.category) ? d.category : _cats.first.code)
-        : _cats.isNotEmpty ? _cats.first.code : 'other';
+        ? (_cats.any((c) => c.code == d.category)
+              ? d.category
+              : _cats.first.code)
+        : _cats.isNotEmpty
+        ? _cats.first.code
+        : 'other';
     _descCtrl = TextEditingController(text: d?.purpose ?? '');
-    _amountCtrl = TextEditingController(text: d != null && d.amount > 0 ? d.amount.toStringAsFixed(2) : '');
+    _amountCtrl = TextEditingController(
+      text: d != null && d.amount > 0 ? d.amount.toStringAsFixed(2) : '',
+    );
     if (d != null && d.customerVendorName.isNotEmpty) {
       _selCustomer = CustomerVendor(id: '', name: d.customerVendorName);
     }
-    _approvedAmountCtrl = TextEditingController(text: d != null && d.approvedAmount > 0 ? d.approvedAmount.toStringAsFixed(2) : '');
+    _approvedAmountCtrl = TextEditingController(
+      text: d != null && d.approvedAmount > 0
+          ? d.approvedAmount.toStringAsFixed(2)
+          : '',
+    );
     _remarkCtrl = TextEditingController(text: d?.remark ?? '');
     _bankNameCtrl = TextEditingController(text: d?.bankName ?? '');
-    _bankAccountNameCtrl = TextEditingController(text: d?.bankAccountName ?? '');
+    _bankAccountNameCtrl = TextEditingController(
+      text: d?.bankAccountName ?? '',
+    );
     _bankAccountCtrl = TextEditingController(text: d?.bankAccount ?? '');
     _taxRate = d?.taxRate ?? 0;
     if (d != null && d.sqMan.isNotEmpty && widget.employees.isNotEmpty) {
@@ -178,10 +220,16 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
     }
     if (d != null) {
       if (d.projectId.isNotEmpty && widget.projects.isNotEmpty) {
-        _selProject = widget.projects.firstWhere((p) => p.id.toString() == d.projectId, orElse: () => widget.projects.first);
+        _selProject = widget.projects.firstWhere(
+          (p) => p.id.toString() == d.projectId,
+          orElse: () => widget.projects.first,
+        );
       }
       if (d.costDeptId.isNotEmpty && widget.costDepts.isNotEmpty) {
-        _selDept = widget.costDepts.firstWhere((dept) => dept.id == d.costDeptId, orElse: () => widget.costDepts.first);
+        _selDept = widget.costDepts.firstWhere(
+          (dept) => dept.id == d.costDeptId,
+          orElse: () => widget.costDepts.first,
+        );
       }
       if (d.attachmentPaths.isNotEmpty) {
         // Restore attachments will be handled after build
@@ -192,6 +240,12 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
     }
     _attachmentCtrl = AttachmentPickerController(maxCount: 9)
       ..addListener(() => setState(() {}));
+    _remarkFocus.addListener(() => _ensureVisible(_remarkFocus));
+    _bankNameFocus.addListener(() => _ensureVisible(_bankNameFocus));
+    _bankAccountNameFocus.addListener(
+      () => _ensureVisible(_bankAccountNameFocus),
+    );
+    _bankAccountFocus.addListener(() => _ensureVisible(_bankAccountFocus));
     _checkAttachHealth();
   }
 
@@ -217,6 +271,10 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
     _bankAccountCtrl.dispose();
     _attachmentCtrl.dispose();
     _scrollCtrl.dispose();
+    _remarkFocus.dispose();
+    _bankNameFocus.dispose();
+    _bankAccountNameFocus.dispose();
+    _bankAccountFocus.dispose();
     super.dispose();
   }
 
@@ -266,81 +324,90 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
   Widget build(BuildContext context) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
 
-    return SafeArea(
-      child: ConstrainedBox(
-        constraints: BoxConstraints(
-          maxHeight: MediaQuery.of(context).size.height * 0.8,
-        ),
-        child: Container(
-          decoration: BoxDecoration(
-            color: colors.bgPage,
-            borderRadius:
-                const BorderRadius.vertical(top: Radius.circular(16)),
+    return AnimatedPadding(
+      padding: EdgeInsets.only(
+        bottom: MediaQuery.of(context).viewInsets.bottom,
+      ),
+      duration: const Duration(milliseconds: 200),
+      child: SafeArea(
+        child: ConstrainedBox(
+          constraints: BoxConstraints(
+            maxHeight: MediaQuery.of(context).size.height * 0.8,
           ),
-          child: Column(
-            mainAxisSize: MainAxisSize.min,
-            crossAxisAlignment: CrossAxisAlignment.stretch,
-            children: [
-              _buildHeader(colors),
-              Flexible(
-                child: GestureDetector(
-                onTap: () => FocusScope.of(context).unfocus(),
-                behavior: HitTestBehavior.translucent,
-                child: SingleChildScrollView(
-                  controller: _scrollCtrl,
-                  keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
-                  padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
-                  child: Column(
-                    mainAxisSize: MainAxisSize.min,
-                    crossAxisAlignment: CrossAxisAlignment.stretch,
-                    children: [
-                      if (_isEdit && widget.initialData!.aeNo.isNotEmpty) ...[
-                        _buildAeInfoCard(colors),
-                        const SizedBox(height: 12),
-                      ],
-                      _buildCategoryCard(colors),
-                      const SizedBox(height: 12),
-                      _buildAcctSubjectCard(colors),
-                      const SizedBox(height: 12),
-                      _buildAmountCard(),
-                      const SizedBox(height: 12),
-                      _buildTaxRateCard(colors),
-                      if ((double.tryParse(_amountCtrl.text) ?? 0) > 0) ...[
-                        const SizedBox(height: 12),
-                        _buildCalcInfo(colors),
-                      ],
-                      const SizedBox(height: 12),
-                      _buildApprovedAmountCard(),
-                      const SizedBox(height: 12),
-                      _buildProjectCard(colors),
-                      const SizedBox(height: 12),
-                      _buildCostDeptCard(colors),
-                      const SizedBox(height: 12),
-                      _buildCustomerCard(colors),
-                      const SizedBox(height: 12),
-                      _buildEmployeeCard(colors),
-                      const SizedBox(height: 12),
-                      _buildBankInfoCard(colors),
-                      const SizedBox(height: 12),
-                      _buildRemarkInput(colors),
-                      const SizedBox(height: 12),
-                      _buildAttachmentCard(colors),
-                    ],
+          child: Container(
+            decoration: BoxDecoration(
+              color: colors.bgPage,
+              borderRadius: const BorderRadius.vertical(
+                top: Radius.circular(16),
+              ),
+            ),
+            child: Column(
+              mainAxisSize: MainAxisSize.min,
+              crossAxisAlignment: CrossAxisAlignment.stretch,
+              children: [
+                _buildHeader(colors),
+                Flexible(
+                  child: GestureDetector(
+                    onTap: () => FocusScope.of(context).unfocus(),
+                    behavior: HitTestBehavior.translucent,
+                    child: SingleChildScrollView(
+                      controller: _scrollCtrl,
+                      keyboardDismissBehavior:
+                          ScrollViewKeyboardDismissBehavior.manual,
+                      padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
+                      child: Column(
+                        mainAxisSize: MainAxisSize.min,
+                        crossAxisAlignment: CrossAxisAlignment.stretch,
+                        children: [
+                          if (_isEdit &&
+                              widget.initialData!.aeNo.isNotEmpty) ...[
+                            _buildAeInfoCard(colors),
+                            const SizedBox(height: 12),
+                          ],
+                          _buildCategoryCard(colors),
+                          const SizedBox(height: 12),
+                          _buildAcctSubjectCard(colors),
+                          const SizedBox(height: 12),
+                          _buildAmountCard(),
+                          const SizedBox(height: 12),
+                          _buildTaxRateCard(colors),
+                          if ((double.tryParse(_amountCtrl.text) ?? 0) > 0) ...[
+                            const SizedBox(height: 12),
+                            _buildCalcInfo(colors),
+                          ],
+                          const SizedBox(height: 12),
+                          _buildApprovedAmountCard(),
+                          const SizedBox(height: 12),
+                          _buildProjectCard(colors),
+                          const SizedBox(height: 12),
+                          _buildCostDeptCard(colors),
+                          const SizedBox(height: 12),
+                          _buildCustomerCard(colors),
+                          const SizedBox(height: 12),
+                          _buildEmployeeCard(colors),
+                          const SizedBox(height: 12),
+                          _buildBankInfoCard(colors),
+                          const SizedBox(height: 12),
+                          _buildRemarkInput(colors),
+                          const SizedBox(height: 12),
+                          _buildAttachmentCard(colors),
+                        ],
+                      ),
+                    ),
                   ),
                 ),
-              ),
-              ),
-              Container(
-                padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
-                decoration: BoxDecoration(
-                  color: colors.bgCard,
-                  border: Border(
-                    top: BorderSide(color: colors.border, width: 0.5),
+                Container(
+                  padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
+                  decoration: BoxDecoration(
+                    color: colors.bgCard,
+                    border: Border(
+                      top: BorderSide(color: colors.border, width: 0.5),
+                    ),
                   ),
+                  child: _buildActions(),
                 ),
-                child: _buildActions(),
-              ),
-            ],
+              ],
+            ),
           ),
         ),
       ),
@@ -416,6 +483,7 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
           TDToast.showText(_l10n.get('noData'), context: context);
           return;
         }
+        FocusManager.instance.primaryFocus?.unfocus();
         TDPicker.showMultiPicker(
           context,
           title: label,
@@ -433,7 +501,12 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
         );
       },
       child: Container(
-        padding: const EdgeInsets.only(left: 16, right: 10, top: 12, bottom: 12),
+        padding: const EdgeInsets.only(
+          left: 16,
+          right: 10,
+          top: 12,
+          bottom: 12,
+        ),
         decoration: BoxDecoration(
           color: tdTheme.bgColorContainer,
           borderRadius: BorderRadius.circular(tdTheme.radiusDefault),
@@ -441,21 +514,62 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
         ),
         child: Row(
           children: [
-            TDText(label, maxLines: 1, overflow: TextOverflow.visible, font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400, style: const TextStyle(letterSpacing: 0)),
+            TDText(
+              label,
+              maxLines: 1,
+              overflow: TextOverflow.visible,
+              font: tdTheme.fontBodyLarge,
+              fontWeight: FontWeight.w400,
+              style: const TextStyle(letterSpacing: 0),
+            ),
             if (required)
-              Padding(padding: const EdgeInsets.only(left: 4), child: TDText('*', font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400, style: TextStyle(color: tdTheme.errorColor6))),
+              Padding(
+                padding: const EdgeInsets.only(left: 4),
+                child: TDText(
+                  '*',
+                  font: tdTheme.fontBodyLarge,
+                  fontWeight: FontWeight.w400,
+                  style: TextStyle(color: tdTheme.errorColor6),
+                ),
+              ),
             const SizedBox(width: 12),
             Expanded(
-              child: Row(mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.max, children: [
-                Flexible(child: TDText(currentLabel, maxLines: 1, overflow: TextOverflow.ellipsis, font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400, textColor: tdTheme.textColorPrimary, textAlign: TextAlign.end)),
-                const SizedBox(width: 4),
-                SizedBox(
-                  width: 18, height: 18,
-                  child: hasValue
-                      ? GestureDetector(onTap: onClear, child: Icon(Icons.close, size: 18, color: tdTheme.textColorPlaceholder))
-                      : Icon(Icons.chevron_right, size: 18, color: tdTheme.textColorPlaceholder),
-                ),
-              ]),
+              child: Row(
+                mainAxisAlignment: MainAxisAlignment.end,
+                mainAxisSize: MainAxisSize.max,
+                children: [
+                  Flexible(
+                    child: TDText(
+                      currentLabel,
+                      maxLines: 1,
+                      overflow: TextOverflow.ellipsis,
+                      font: tdTheme.fontBodyLarge,
+                      fontWeight: FontWeight.w400,
+                      textColor: tdTheme.textColorPrimary,
+                      textAlign: TextAlign.end,
+                    ),
+                  ),
+                  const SizedBox(width: 4),
+                  SizedBox(
+                    width: 18,
+                    height: 18,
+                    child: hasValue
+                        ? GestureDetector(
+                            onTap: onClear,
+                            child: Icon(
+                              Icons.close,
+                              size: 18,
+                              color: tdTheme.textColorPlaceholder,
+                            ),
+                          )
+                        : Icon(
+                            Icons.chevron_right,
+                            size: 18,
+                            color: tdTheme.textColorPlaceholder,
+                          ),
+                  ),
+                ],
+              ),
             ),
           ],
         ),
@@ -484,6 +598,7 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
     required AppColorsExtension colors,
     TextInputType? keyboardType,
     List<TextInputFormatter>? inputFormatters,
+    FocusNode? focusNode,
   }) {
     final tdTheme = TDTheme.of(context);
     final hasValue = controller.text.isNotEmpty;
@@ -496,40 +611,71 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
       ),
       child: Row(
         children: [
-          TDText(label, maxLines: 1, overflow: TextOverflow.visible, font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400, style: const TextStyle(letterSpacing: 0)),
+          TDText(
+            label,
+            maxLines: 1,
+            overflow: TextOverflow.visible,
+            font: tdTheme.fontBodyLarge,
+            fontWeight: FontWeight.w400,
+            style: const TextStyle(letterSpacing: 0),
+          ),
           if (required)
-            Padding(padding: const EdgeInsets.only(left: 4), child: TDText('*', font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400, style: TextStyle(color: tdTheme.errorColor6))),
+            Padding(
+              padding: const EdgeInsets.only(left: 4),
+              child: TDText(
+                '*',
+                font: tdTheme.fontBodyLarge,
+                fontWeight: FontWeight.w400,
+                style: TextStyle(color: tdTheme.errorColor6),
+              ),
+            ),
           const SizedBox(width: 12),
           Expanded(
-            child: Row(mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.max, children: [
-              Flexible(
-                child: TextField(
-                  controller: controller,
-                  textAlign: TextAlign.end,
-                  keyboardType: keyboardType,
-                  inputFormatters: inputFormatters,
-                  style: TextStyle(fontSize: 16, color: colors.textPrimary),
-                  decoration: InputDecoration(
-                    hintText: hintText,
-                    hintStyle: TextStyle(fontSize: 16, color: colors.textPlaceholder),
-                    border: InputBorder.none,
-                    isDense: true,
-                    contentPadding: EdgeInsets.zero,
+            child: Row(
+              mainAxisAlignment: MainAxisAlignment.end,
+              mainAxisSize: MainAxisSize.max,
+              children: [
+                Flexible(
+                  child: TextField(
+                    controller: controller,
+                    focusNode: focusNode,
+                    textAlign: TextAlign.end,
+                    keyboardType: keyboardType,
+                    inputFormatters: inputFormatters,
+                    style: TextStyle(fontSize: 16, color: colors.textPrimary),
+                    decoration: InputDecoration(
+                      hintText: hintText,
+                      hintStyle: TextStyle(
+                        fontSize: 16,
+                        color: colors.textPlaceholder,
+                      ),
+                      border: InputBorder.none,
+                      isDense: true,
+                      contentPadding: EdgeInsets.zero,
+                    ),
+                    onChanged: (_) => setState(() {}),
                   ),
-                  onChanged: (_) => setState(() {}),
                 ),
-              ),
-              const SizedBox(width: 4),
-              SizedBox(
-                width: 18, height: 18,
-                child: hasValue
-                    ? GestureDetector(
-                        onTap: () { controller.clear(); setState(() {}); },
-                        child: Icon(Icons.close, size: 18, color: tdTheme.textColorPlaceholder),
-                      )
-                    : null,
-              ),
-            ]),
+                const SizedBox(width: 4),
+                SizedBox(
+                  width: 18,
+                  height: 18,
+                  child: hasValue
+                      ? GestureDetector(
+                          onTap: () {
+                            controller.clear();
+                            setState(() {});
+                          },
+                          child: Icon(
+                            Icons.close,
+                            size: 18,
+                            color: tdTheme.textColorPlaceholder,
+                          ),
+                        )
+                      : null,
+                ),
+              ],
+            ),
           ),
         ],
       ),
@@ -545,14 +691,15 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
       hintText: '>0',
       colors: Theme.of(context).extension<AppColorsExtension>()!,
       keyboardType: const TextInputType.numberWithOptions(decimal: true),
-      inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$'))],
+      inputFormatters: [
+        FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$')),
+      ],
     );
   }
 
   // ── 税率 ──
   Widget _buildTaxRateCard(AppColorsExtension colors) {
-    final currentLabel =
-        '${(_taxRate * 100).toStringAsFixed(0)}%';
+    final currentLabel = '${(_taxRate * 100).toStringAsFixed(0)}%';
     return _pickerCard(
       label: _l10n.get('taxRate'),
       required: true,
@@ -616,11 +763,28 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
   }
 
   Widget _readOnlyRow(TDThemeData tdTheme, String label, String value) {
-    return Row(children: [
-      TDText(label, font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400, style: const TextStyle(letterSpacing: 0)),
-      const SizedBox(width: 12),
-      Expanded(child: TDText(value, maxLines: 1, overflow: TextOverflow.ellipsis, font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400, textColor: tdTheme.textColorPrimary, textAlign: TextAlign.end)),
-    ]);
+    return Row(
+      children: [
+        TDText(
+          label,
+          font: tdTheme.fontBodyLarge,
+          fontWeight: FontWeight.w400,
+          style: const TextStyle(letterSpacing: 0),
+        ),
+        const SizedBox(width: 12),
+        Expanded(
+          child: TDText(
+            value,
+            maxLines: 1,
+            overflow: TextOverflow.ellipsis,
+            font: tdTheme.fontBodyLarge,
+            fontWeight: FontWeight.w400,
+            textColor: tdTheme.textColorPrimary,
+            textAlign: TextAlign.end,
+          ),
+        ),
+      ],
+    );
   }
 
   BoxDecoration _cardDecoration(TDThemeData tdTheme) => BoxDecoration(
@@ -635,7 +799,9 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
     return _pickerCard(
       label: _l10n.get('applicant'),
       required: false,
-      currentLabel: _selEmployee != null ? '${_selEmployee!.salNo}/${_selEmployee!.name}' : _l10n.get('pleaseSelect'),
+      currentLabel: _selEmployee != null
+          ? '${_selEmployee!.salNo}/${_selEmployee!.name}'
+          : _l10n.get('pleaseSelect'),
       labels: employees.map((e) => '${e.salNo}/${e.name}').toList(),
       colors: colors,
       onSelected: (idx) => setState(() {
@@ -644,12 +810,14 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
         _bankAccountNameCtrl.text = _selEmployee!.accName;
         _bankAccountCtrl.text = _selEmployee!.bnkId;
       }),
-      onClear: _selEmployee != null ? () => setState(() {
-        _selEmployee = null;
-        _bankNameCtrl.clear();
-        _bankAccountNameCtrl.clear();
-        _bankAccountCtrl.clear();
-      }) : null,
+      onClear: _selEmployee != null
+          ? () => setState(() {
+              _selEmployee = null;
+              _bankNameCtrl.clear();
+              _bankAccountNameCtrl.clear();
+              _bankAccountCtrl.clear();
+            })
+          : null,
     );
   }
 
@@ -659,11 +827,15 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
     return _pickerCard(
       label: _l10n.get('customerVendor'),
       required: false,
-      currentLabel: _selCustomer != null ? '${_selCustomer!.id}/${_selCustomer!.name}' : _l10n.get('pleaseSelect'),
+      currentLabel: _selCustomer != null
+          ? '${_selCustomer!.id}/${_selCustomer!.name}'
+          : _l10n.get('pleaseSelect'),
       labels: vendors.map((v) => '${v.id}/${v.name}').toList(),
       colors: colors,
       onSelected: (idx) => setState(() => _selCustomer = vendors[idx]),
-      onClear: _selCustomer != null ? () => setState(() => _selCustomer = null) : null,
+      onClear: _selCustomer != null
+          ? () => setState(() => _selCustomer = null)
+          : null,
     );
   }
 
@@ -676,7 +848,9 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
       hintText: '0',
       colors: Theme.of(context).extension<AppColorsExtension>()!,
       keyboardType: const TextInputType.numberWithOptions(decimal: true),
-      inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$'))],
+      inputFormatters: [
+        FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$')),
+      ],
     );
   }
 
@@ -685,6 +859,7 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
     final tdTheme = TDTheme.of(context);
     return TDTextarea(
       controller: _remarkCtrl,
+      focusNode: _remarkFocus,
       label: _l10n.get('remark'),
       hintText: _l10n.get('enterRemark'),
       maxLines: 3,
@@ -726,15 +901,27 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
         ),
         const SizedBox(height: 4),
         if (!_attachAvailable)
-          TDText(_l10n.get('attachServiceUnavailable'),
-              font: tdTheme.fontBodyMedium, fontWeight: FontWeight.w400,
-              textColor: tdTheme.textColorPlaceholder)
+          TDText(
+            _l10n.get('attachServiceUnavailable'),
+            font: tdTheme.fontBodyMedium,
+            fontWeight: FontWeight.w400,
+            textColor: tdTheme.textColorPlaceholder,
+          )
         else
           AttachmentPicker(
             controller: _attachmentCtrl,
             maxImageSizeMB: 10,
             maxFileSizeMB: 20,
-            allowedExtensions: const ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'],
+            allowedExtensions: const [
+              'pdf',
+              'doc',
+              'docx',
+              'xls',
+              'xlsx',
+              'ppt',
+              'pptx',
+              'txt',
+            ],
             onFileRejected: (file, reason) {
               if (context.mounted) TDToast.showText(reason, context: context);
             },
@@ -767,11 +954,25 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
       ),
       child: Row(
         children: [
-          TDText(label, maxLines: 1, overflow: TextOverflow.visible, font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400, style: const TextStyle(letterSpacing: 0)),
+          TDText(
+            label,
+            maxLines: 1,
+            overflow: TextOverflow.visible,
+            font: tdTheme.fontBodyLarge,
+            fontWeight: FontWeight.w400,
+            style: const TextStyle(letterSpacing: 0),
+          ),
           const SizedBox(width: 12),
           Expanded(
-            child: TDText(value, maxLines: 1, overflow: TextOverflow.ellipsis, font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400,
-              textColor: tdTheme.textColorPrimary, textAlign: TextAlign.end),
+            child: TDText(
+              value,
+              maxLines: 1,
+              overflow: TextOverflow.ellipsis,
+              font: tdTheme.fontBodyLarge,
+              fontWeight: FontWeight.w400,
+              textColor: tdTheme.textColorPrimary,
+              textAlign: TextAlign.end,
+            ),
           ),
         ],
       ),
@@ -784,11 +985,15 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
     return _pickerCard(
       label: _l10n.get('relatedProject'),
       required: false,
-      currentLabel: _selProject != null ? '${_selProject!.id}/${_selProject!.name}' : _l10n.get('pleaseSelect'),
+      currentLabel: _selProject != null
+          ? '${_selProject!.id}/${_selProject!.name}'
+          : _l10n.get('pleaseSelect'),
       labels: projects.map((p) => '${p.id}/${p.name}').toList(),
       colors: colors,
       onSelected: (idx) => setState(() => _selProject = projects[idx]),
-      onClear: _selProject != null ? () => setState(() => _selProject = null) : null,
+      onClear: _selProject != null
+          ? () => setState(() => _selProject = null)
+          : null,
     );
   }
 
@@ -798,7 +1003,9 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
     return _pickerCard(
       label: _l10n.get('costDept'),
       required: false,
-      currentLabel: _selDept != null ? '${_selDept!.id}/${_selDept!.name}' : _l10n.get('pleaseSelect'),
+      currentLabel: _selDept != null
+          ? '${_selDept!.id}/${_selDept!.name}'
+          : _l10n.get('pleaseSelect'),
       labels: depts.map((d) => '${d.id}/${d.name}').toList(),
       colors: colors,
       onSelected: (idx) => setState(() => _selDept = depts[idx]),
@@ -811,11 +1018,32 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
     return Column(
       crossAxisAlignment: CrossAxisAlignment.start,
       children: [
-        _inputCard(label: _l10n.get('bankName'), required: false, controller: _bankNameCtrl, hintText: _l10n.get('pleaseEnter'), colors: colors),
+        _inputCard(
+          label: _l10n.get('bankName'),
+          required: false,
+          controller: _bankNameCtrl,
+          hintText: _l10n.get('pleaseEnter'),
+          colors: colors,
+          focusNode: _bankNameFocus,
+        ),
         const SizedBox(height: 12),
-        _inputCard(label: _l10n.get('bankAccountName'), required: false, controller: _bankAccountNameCtrl, hintText: _l10n.get('pleaseEnter'), colors: colors),
+        _inputCard(
+          label: _l10n.get('bankAccountName'),
+          required: false,
+          controller: _bankAccountNameCtrl,
+          hintText: _l10n.get('pleaseEnter'),
+          colors: colors,
+          focusNode: _bankAccountNameFocus,
+        ),
         const SizedBox(height: 12),
-        _inputCard(label: _l10n.get('bankAccount'), required: false, controller: _bankAccountCtrl, hintText: _l10n.get('pleaseEnter'), colors: colors),
+        _inputCard(
+          label: _l10n.get('bankAccount'),
+          required: false,
+          controller: _bankAccountCtrl,
+          hintText: _l10n.get('pleaseEnter'),
+          colors: colors,
+          focusNode: _bankAccountFocus,
+        ),
       ],
     );
   }

+ 34 - 26
lib/features/expense_apply/expense_apply_create_page.dart

@@ -169,32 +169,35 @@ class _ExpenseApplyCreatePageState
 
   @override
   void didChangeAppLifecycleState(AppLifecycleState state) {
-    if (state == AppLifecycleState.resumed && _isPoppingToNative) {
-      _isPoppingToNative = false;
-      HostAppChannel.refresh();
-      // 重置表单数据
-      _urgency = Urgency.normal.value;
-      _purposeController.clear();
-      _validUntil = '';
-      _referenceNoController.clear();
-      _remarkController.clear();
-      _details.clear();
-      _detailIdCounter = 1;
-      _attachmentController.clear();
-      _attachAvailable = false;
-      _addingDetail = false;
-      _selectedDeptId = '';
-      _selectedDeptName = '';
-      // 重置参考数据
-      _costTypes = []; _projects = []; _departments = [];
-      _refDataFuture = null;
-      _refDataLoading = true;
-      // 重置草稿状态
-      _draftHandled = false;
-      _draftFuture = DraftStorage.has(_draftKey);
-      // 重新加载
-      _loadRefData();
-      _checkAttachHealth();
+    if (state == AppLifecycleState.resumed) {
+      FocusScope.of(context).unfocus();
+      if (_isPoppingToNative) {
+        _isPoppingToNative = false;
+        HostAppChannel.refresh();
+        // 重置表单数据
+        _urgency = Urgency.normal.value;
+        _purposeController.clear();
+        _validUntil = '';
+        _referenceNoController.clear();
+        _remarkController.clear();
+        _details.clear();
+        _detailIdCounter = 1;
+        _attachmentController.clear();
+        _attachAvailable = false;
+        _addingDetail = false;
+        _selectedDeptId = '';
+        _selectedDeptName = '';
+        // 重置参考数据
+        _costTypes = []; _projects = []; _departments = [];
+        _refDataFuture = null;
+        _refDataLoading = true;
+        // 重置草稿状态
+        _draftHandled = false;
+        _draftFuture = DraftStorage.has(_draftKey);
+        // 重新加载
+        _loadRefData();
+        _checkAttachHealth();
+      }
     }
   }
 
@@ -334,6 +337,7 @@ class _ExpenseApplyCreatePageState
   void _showDraftDialog() {
     final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    FocusManager.instance.primaryFocus?.unfocus();
     showDialog(
       context: context,
       barrierDismissible: false,
@@ -735,6 +739,7 @@ class _ExpenseApplyCreatePageState
         remark: d.remark,
       );
     }
+    FocusManager.instance.primaryFocus?.unfocus();
     final result = await ExpenseApplyDetailDialog.show(
       // ignore: use_build_context_synchronously
       context,
@@ -832,6 +837,7 @@ class _ExpenseApplyCreatePageState
     final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final labels = _departments.map((d) => '${d.dep}/${d.name}').toList();
+    FocusManager.instance.primaryFocus?.unfocus();
     TDPicker.showMultiPicker(
       context,
       title: l10n.get('applyDept'),
@@ -1017,6 +1023,7 @@ class _ExpenseApplyCreatePageState
   }
 
   void _forcePop() {
+    FocusManager.instance.primaryFocus?.unfocus();
     final router = GoRouter.of(context);
     if (router.canPop()) {
       router.pop();
@@ -1060,6 +1067,7 @@ class _ExpenseApplyCreatePageState
     VoidCallback onConfirm,
   ) {
     _unfocus();
+    FocusManager.instance.primaryFocus?.unfocus();
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     showDialog(
       context: context,

+ 6 - 1
lib/features/expense_apply/expense_apply_list_page.dart

@@ -46,6 +46,7 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage>
   @override
   void didChangeAppLifecycleState(AppLifecycleState state) {
     if (state == AppLifecycleState.resumed) {
+      FocusScope.of(context).unfocus();
       _keywordCtrl.clear();
       final now = DateTime.now();
       _startDateCtrl.text = '${now.year}-${now.month.toString().padLeft(2, '0')}-01';
@@ -68,6 +69,7 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage>
   }
 
   void _pickDate(TextEditingController ctrl) {
+    FocusManager.instance.primaryFocus?.unfocus();
     final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final now = DateTime.now();
@@ -112,7 +114,10 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage>
     setNavBarTitle(context, ref, NavBarConfig(
       title: l10n.get('expenseApplyList'),
       showBack: true,
-      onBack: () => SystemNavigator.pop(),
+      onBack: () {
+        FocusManager.instance.primaryFocus?.unfocus();
+        SystemNavigator.pop();
+      },
       showRight: true,
       rightWidget: GestureDetector(
         onTap: () => context.push('/expense-apply/create'),

+ 153 - 87
lib/features/expense_apply/widgets/expense_apply_detail_dialog.dart

@@ -88,8 +88,7 @@ class ExpenseApplyDetailDialog extends StatefulWidget {
       _ExpenseApplyDetailDialogState();
 }
 
-class _ExpenseApplyDetailDialogState
-    extends State<ExpenseApplyDetailDialog> {
+class _ExpenseApplyDetailDialogState extends State<ExpenseApplyDetailDialog> {
   // 费用类别
   late CostCategory _selCat;
   // 关联项目
@@ -133,20 +132,36 @@ class _ExpenseApplyDetailDialogState
     }
     _selCat = _cats.isNotEmpty
         ? (d != null
-            ? _cats.firstWhere((c) => c.code == d.category, orElse: () => _cats.first)
-            : _cats.first)
-        : const CostCategory(code: '', nameKey: '', acctSubjectId: '', acctSubjectName: '');
+              ? _cats.firstWhere(
+                  (c) => c.code == d.category,
+                  orElse: () => _cats.first,
+                )
+              : _cats.first)
+        : const CostCategory(
+            code: '',
+            nameKey: '',
+            acctSubjectId: '',
+            acctSubjectName: '',
+          );
     _selProject = (d != null && d.projectId != 0 && _projects.isNotEmpty)
-        ? _projects.firstWhere((p) => p.id == d.projectId, orElse: () => _projects.first)
+        ? _projects.firstWhere(
+            (p) => p.id == d.projectId,
+            orElse: () => _projects.first,
+          )
         : null;
     _selDept = (d != null && d.costDeptId.isNotEmpty && _depts.isNotEmpty)
-        ? _depts.firstWhere((dept) => dept.id == d.costDeptId, orElse: () => _depts.first)
+        ? _depts.firstWhere(
+            (dept) => dept.id == d.costDeptId,
+            orElse: () => _depts.first,
+          )
         : null;
     _startDate = d?.startDate ?? '';
     _endDate = d?.endDate ?? '';
     if (d != null) {
       _purposeCtrl.text = d.purpose;
-      _amountCtrl.text = d.estimatedAmount > 0 ? d.estimatedAmount.toStringAsFixed(2) : '';
+      _amountCtrl.text = d.estimatedAmount > 0
+          ? d.estimatedAmount.toStringAsFixed(2)
+          : '';
       _remarkCtrl.text = d.remark;
     }
     _purposeFocus.addListener(() => _ensureVisible(_purposeFocus));
@@ -156,18 +171,30 @@ class _ExpenseApplyDetailDialogState
 
   void _ensureVisible(FocusNode node) {
     if (!node.hasFocus) return;
-    // 延迟一帧,等键盘 viewInsets 生效后再滚动
+    _doEnsureVisible(node, 0, -1);
+  }
+
+  void _doEnsureVisible(FocusNode node, int attempt, double lastInsets) {
+    if (attempt >= 15) return;
     WidgetsBinding.instance.addPostFrameCallback((_) {
-      if (node.hasFocus && _scrollCtrl.hasClients) {
-        final ctx = node.context;
-        if (ctx != null) {
-          Scrollable.ensureVisible(
-            ctx,
-            alignment: 0.3,
-            duration: const Duration(milliseconds: 300),
-          );
-        }
+      if (!mounted || !node.hasFocus || !_scrollCtrl.hasClients) return;
+      final insets = MediaQuery.of(context).viewInsets.bottom;
+      if (insets != lastInsets) {
+        _doEnsureVisible(node, attempt + 1, insets);
+        return;
       }
+      // viewInsets 已稳定,但 AnimatedPadding 动画(200ms)可能还在进行
+      // 等 250ms 确保 dialog 位置完全就位再滚动
+      Future.delayed(const Duration(milliseconds: 500), () {
+        if (!mounted || !node.hasFocus || !_scrollCtrl.hasClients) return;
+        final ctx = node.context;
+        if (ctx == null) return;
+        Scrollable.ensureVisible(
+          ctx,
+          alignment: 0.5,
+          duration: const Duration(milliseconds: 300),
+        );
+      });
     });
   }
 
@@ -192,10 +219,7 @@ class _ExpenseApplyDetailDialogState
     if (_startDate.isNotEmpty &&
         _endDate.isNotEmpty &&
         _startDate.compareTo(_endDate) > 0) {
-      TDToast.showText(
-        _l10n.get('startDateNotAfterEndDate'),
-        context: context,
-      );
+      TDToast.showText(_l10n.get('startDateNotAfterEndDate'), context: context);
       return;
     }
     Navigator.pop(
@@ -222,16 +246,22 @@ class _ExpenseApplyDetailDialogState
   Widget build(BuildContext context) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
 
-    return SafeArea(
-      child: ConstrainedBox(
+    return AnimatedPadding(
+      padding: EdgeInsets.only(
+        bottom: MediaQuery.of(context).viewInsets.bottom,
+      ),
+      duration: const Duration(milliseconds: 200),
+      child: SafeArea(
+        child: ConstrainedBox(
           constraints: BoxConstraints(
             maxHeight: MediaQuery.of(context).size.height * 0.8,
           ),
           child: Container(
             decoration: BoxDecoration(
               color: colors.bgPage,
-              borderRadius:
-                  const BorderRadius.vertical(top: Radius.circular(16)),
+              borderRadius: const BorderRadius.vertical(
+                top: Radius.circular(16),
+              ),
             ),
             child: Column(
               mainAxisSize: MainAxisSize.min,
@@ -240,47 +270,49 @@ class _ExpenseApplyDetailDialogState
                 _buildHeader(colors),
                 Flexible(
                   child: GestureDetector(
-                  onTap: () => FocusScope.of(context).unfocus(),
-                  behavior: HitTestBehavior.translucent,
-                  child: SingleChildScrollView(
-                  controller: _scrollCtrl,
-                  keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
-                  padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
-                  child: Column(
-                    mainAxisSize: MainAxisSize.min,
-                    crossAxisAlignment: CrossAxisAlignment.stretch,
-                    children: [
-                      _buildCategoryPicker(colors),
-                      const SizedBox(height: 12),
-                      _buildAcctSubjectCard(colors),
-                      const SizedBox(height: 12),
-                      _buildAmountInput(colors),
-                      const SizedBox(height: 12),
-                      _buildProjectPicker(colors),
-                      const SizedBox(height: 12),
-                      _buildCostDeptPicker(colors),
-                      const SizedBox(height: 12),
-                      _buildStartDatePicker(colors),
-                      const SizedBox(height: 12),
-                      _buildEndDatePicker(colors),
-                      const SizedBox(height: 12),
-                      _buildRemarkInput(colors),
-                    ],
+                    onTap: () => FocusScope.of(context).unfocus(),
+                    behavior: HitTestBehavior.translucent,
+                    child: SingleChildScrollView(
+                      controller: _scrollCtrl,
+                      keyboardDismissBehavior:
+                          ScrollViewKeyboardDismissBehavior.manual,
+                      padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
+                      child: Column(
+                        mainAxisSize: MainAxisSize.min,
+                        crossAxisAlignment: CrossAxisAlignment.stretch,
+                        children: [
+                          _buildCategoryPicker(colors),
+                          const SizedBox(height: 12),
+                          _buildAcctSubjectCard(colors),
+                          const SizedBox(height: 12),
+                          _buildAmountInput(colors),
+                          const SizedBox(height: 12),
+                          _buildProjectPicker(colors),
+                          const SizedBox(height: 12),
+                          _buildCostDeptPicker(colors),
+                          const SizedBox(height: 12),
+                          _buildStartDatePicker(colors),
+                          const SizedBox(height: 12),
+                          _buildEndDatePicker(colors),
+                          const SizedBox(height: 12),
+                          _buildRemarkInput(colors),
+                        ],
+                      ),
+                    ),
                   ),
                 ),
-              ),
-              ),
-              Container(
-                padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
-                decoration: BoxDecoration(
-                  color: colors.bgCard,
-                  border: Border(
-                    top: BorderSide(color: colors.border, width: 0.5),
+                Container(
+                  padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
+                  decoration: BoxDecoration(
+                    color: colors.bgCard,
+                    border: Border(
+                      top: BorderSide(color: colors.border, width: 0.5),
+                    ),
                   ),
+                  child: _buildActions(),
                 ),
-                child: _buildActions(),
-              ),
-            ],
+              ],
+            ),
           ),
         ),
       ),
@@ -356,6 +388,7 @@ class _ExpenseApplyDetailDialogState
           TDToast.showText(_l10n.get('noData'), context: context);
           return;
         }
+        FocusManager.instance.primaryFocus?.unfocus();
         TDPicker.showMultiPicker(
           context,
           title: label,
@@ -374,7 +407,10 @@ class _ExpenseApplyDetailDialogState
       },
       child: Container(
         padding: const EdgeInsets.only(
-          left: 16, right: 10, top: 12, bottom: 12,
+          left: 16,
+          right: 10,
+          top: 12,
+          bottom: 12,
         ),
         decoration: BoxDecoration(
           color: tdTheme.bgColorContainer,
@@ -420,13 +456,22 @@ class _ExpenseApplyDetailDialogState
                   ),
                   const SizedBox(width: 4),
                   SizedBox(
-                    width: 18, height: 18,
+                    width: 18,
+                    height: 18,
                     child: hasValue
                         ? GestureDetector(
                             onTap: onClear,
-                            child: Icon(Icons.close, size: 18, color: tdTheme.textColorPlaceholder),
+                            child: Icon(
+                              Icons.close,
+                              size: 18,
+                              color: tdTheme.textColorPlaceholder,
+                            ),
                           )
-                        : Icon(Icons.chevron_right, size: 18, color: tdTheme.textColorPlaceholder),
+                        : Icon(
+                            Icons.chevron_right,
+                            size: 18,
+                            color: tdTheme.textColorPlaceholder,
+                          ),
                   ),
                 ],
               ),
@@ -445,9 +490,7 @@ class _ExpenseApplyDetailDialogState
   }) {
     final tdTheme = TDTheme.of(context);
     return Container(
-      padding: const EdgeInsets.only(
-        left: 16, right: 16, top: 12, bottom: 12,
-      ),
+      padding: const EdgeInsets.only(left: 16, right: 16, top: 12, bottom: 12),
       decoration: BoxDecoration(
         color: tdTheme.bgColorContainer,
         borderRadius: BorderRadius.circular(tdTheme.radiusDefault),
@@ -506,11 +549,15 @@ class _ExpenseApplyDetailDialogState
     return _pickerCard(
       label: _l10n.get('relatedProject'),
       required: false,
-      currentLabel: _selProject != null ? '${_selProject!.id}/${_selProject!.name}' : _l10n.get('pleaseSelect'),
+      currentLabel: _selProject != null
+          ? '${_selProject!.id}/${_selProject!.name}'
+          : _l10n.get('pleaseSelect'),
       labels: _projects.map((p) => '${p.id}/${p.name}').toList(),
       colors: colors,
       onSelected: (idx) => setState(() => _selProject = _projects[idx]),
-      onClear: _selProject != null ? () => setState(() => _selProject = null) : null,
+      onClear: _selProject != null
+          ? () => setState(() => _selProject = null)
+          : null,
     );
   }
 
@@ -519,7 +566,9 @@ class _ExpenseApplyDetailDialogState
     return _pickerCard(
       label: _l10n.get('costDept'),
       required: false,
-      currentLabel: _selDept != null ? '${_selDept!.id}/${_selDept!.name}' : _l10n.get('pleaseSelect'),
+      currentLabel: _selDept != null
+          ? '${_selDept!.id}/${_selDept!.name}'
+          : _l10n.get('pleaseSelect'),
       labels: _depts.map((d) => '${d.id}/${d.name}').toList(),
       colors: colors,
       onSelected: (idx) => setState(() => _selDept = _depts[idx]),
@@ -535,7 +584,9 @@ class _ExpenseApplyDetailDialogState
       hint: _l10n.get('pleaseSelect'),
       colors: colors,
       onPick: (d) => setState(() => _startDate = d),
-      onClear: _startDate.isNotEmpty ? () => setState(() => _startDate = '') : null,
+      onClear: _startDate.isNotEmpty
+          ? () => setState(() => _startDate = '')
+          : null,
     );
   }
 
@@ -565,7 +616,10 @@ class _ExpenseApplyDetailDialogState
       onTap: () => _pickDate(onPick),
       child: Container(
         padding: const EdgeInsets.only(
-          left: 16, right: 10, top: 12, bottom: 12,
+          left: 16,
+          right: 10,
+          top: 12,
+          bottom: 12,
         ),
         decoration: BoxDecoration(
           color: tdTheme.bgColorContainer,
@@ -603,13 +657,22 @@ class _ExpenseApplyDetailDialogState
                   ),
                   const SizedBox(width: 4),
                   SizedBox(
-                    width: 18, height: 18,
+                    width: 18,
+                    height: 18,
                     child: hasValue
                         ? GestureDetector(
                             onTap: onClear,
-                            child: Icon(Icons.close, size: 18, color: tdTheme.textColorPlaceholder),
+                            child: Icon(
+                              Icons.close,
+                              size: 18,
+                              color: tdTheme.textColorPlaceholder,
+                            ),
                           )
-                        : Icon(Icons.chevron_right, size: 18, color: tdTheme.textColorPlaceholder),
+                        : Icon(
+                            Icons.chevron_right,
+                            size: 18,
+                            color: tdTheme.textColorPlaceholder,
+                          ),
                   ),
                 ],
               ),
@@ -624,12 +687,18 @@ class _ExpenseApplyDetailDialogState
     final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final now = DateTime.now();
+    FocusManager.instance.primaryFocus?.unfocus();
     TDPicker.showDatePicker(
       context,
       title: l10n.get('selectDate'),
       backgroundColor: colors.bgCard,
-      useYear: true, useMonth: true, useDay: true,
-      useHour: false, useMinute: false, useSecond: false, useWeekDay: false,
+      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],
@@ -678,16 +747,13 @@ class _ExpenseApplyDetailDialogState
               controller: _amountCtrl,
               focusNode: _amountFocus,
               textAlign: TextAlign.end,
-              keyboardType: const TextInputType.numberWithOptions(decimal: true),
-              style: TextStyle(
-                fontSize: 16,
-                color: tdTheme.textColorPrimary,
+              keyboardType: const TextInputType.numberWithOptions(
+                decimal: true,
               ),
+              style: TextStyle(fontSize: 16, color: tdTheme.textColorPrimary),
               decoration: InputDecoration(
                 hintText: '>0',
-                hintStyle: TextStyle(
-                  color: tdTheme.textColorPlaceholder,
-                ),
+                hintStyle: TextStyle(color: tdTheme.textColorPlaceholder),
                 border: InputBorder.none,
                 isDense: true,
                 contentPadding: const EdgeInsets.symmetric(vertical: 12),