Selaa lähdekoodia

doing #19407 修复Scaffold嵌套问题等

chengc 2 viikkoa sitten
vanhempi
commit
54ef5c1b26

+ 1 - 1
lib/core/router/app_router.dart

@@ -45,7 +45,7 @@ void _popToNative() {
 
 void _smartBack(BuildContext context) {
   final r = GoRouter.of(context);
-  if (r.canPop()) r.pop(); else SystemNavigator.pop();
+  if (r.canPop()) { r.pop(); } else { SystemNavigator.pop(); }
 }
 
 GoRouter createAppRouter() => GoRouter(

+ 25 - 10
lib/core/storage/draft_storage.dart

@@ -1,4 +1,5 @@
 import 'dart:convert';
+import 'package:flutter/foundation.dart';
 import 'package:flutter/services.dart';
 
 /// 本地草稿持久化服务,通过 MethodChannel 调用原生 SharedPreferences / NSUserDefaults。
@@ -10,21 +11,30 @@ class DraftStorage {
 
   /// 保存草稿
   static Future<void> save(String key, Map<String, dynamic> data) async {
-    await _channel.invokeMethod('setString', {
-      'key': _prefix + key,
-      'value': jsonEncode(data),
-    });
+    final fullKey = _prefix + key;
+    try {
+      await _channel.invokeMethod('setString', {
+        'key': fullKey,
+        'value': jsonEncode(data),
+      });
+      debugPrint('[DraftStorage] save($fullKey) success');
+    } catch (e) {
+      debugPrint('[DraftStorage] save($fullKey) error: $e');
+    }
   }
 
   /// 读取草稿,不存在返回 null
   static Future<Map<String, dynamic>?> load(String key) async {
+    final fullKey = _prefix + key;
     try {
       final raw = await _channel.invokeMethod<String>('getString', {
-        'key': _prefix + key,
+        'key': fullKey,
       });
+      debugPrint('[DraftStorage] load($fullKey) = ${raw != null ? 'found (${raw.length} chars)' : 'null'}');
       if (raw == null || raw.isEmpty) return null;
       return jsonDecode(raw) as Map<String, dynamic>;
-    } catch (_) {
+    } catch (e) {
+      debugPrint('[DraftStorage] load($fullKey) error: $e');
       return null;
     }
   }
@@ -32,11 +42,16 @@ class DraftStorage {
   /// 是否有草稿
   static Future<bool> has(String key) async {
     try {
-      final result = await _channel.invokeMethod<bool>('hasKey', {
-        'key': _prefix + key,
+      final fullKey = _prefix + key;
+      final result = await _channel.invokeMethod('hasKey', {
+        'key': fullKey,
       });
-      return result ?? false;
-    } catch (_) {
+      // iOS 端 @(YES) 在某些 Flutter 版本被编解码为 int(0/1) 而非 bool
+      final exists = result == true || result == 1;
+      debugPrint('[DraftStorage] has($fullKey) = $exists (raw: $result, type: ${result.runtimeType})');
+      return exists;
+    } catch (e) {
+      debugPrint('[DraftStorage] has($key) error: $e');
       return false;
     }
   }

+ 1 - 0
lib/features/admin/admin_permissions_page.dart

@@ -112,6 +112,7 @@ class _AdminPermissionsPageState extends ConsumerState<AdminPermissionsPage> {
   @override
   Widget build(BuildContext context) {
     //final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    // ignore: unused_local_variable
     final l10n = AppLocalizations.of(context);
     return Column(
       children: [

+ 0 - 83
lib/features/announcement/announcement_create_page.dart

@@ -313,89 +313,6 @@ class _AnnouncementCreatePageState
     );
   }
 
-  void _showPreview() {
-    final colors = Theme.of(context).extension<AppColorsExtension>()!;
-    final l10n = AppLocalizations.of(context);
-    showDialog(
-      context: context,
-      barrierDismissible: true,
-      builder: (ctx) => Dialog(
-        insetPadding: const EdgeInsets.all(16),
-        child: Container(
-          width: double.infinity,
-          height: MediaQuery.of(context).size.height * 0.85,
-          padding: const EdgeInsets.all(16),
-          child: Column(
-            crossAxisAlignment: CrossAxisAlignment.start,
-            children: [
-              Row(
-                mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                children: [
-                  Text(
-                    l10n.get('previewTitle'),
-                    style: TextStyle(
-                      fontSize: 16,
-                      fontWeight: FontWeight.w600,
-                      color: colors.textPrimary,
-                    ),
-                  ),
-                  GestureDetector(
-                    onTap: () => Navigator.pop(ctx),
-                    child: const Icon(Icons.close, size: 20),
-                  ),
-                ],
-              ),
-              const TDDivider(),
-              Expanded(
-                child: SingleChildScrollView(
-                  child: Column(
-                    crossAxisAlignment: CrossAxisAlignment.start,
-                    children: [
-                      Container(width: 60, height: 4, color: colors.danger),
-                      const SizedBox(height: 16),
-                      Text(
-                        _titleCtrl.text.isEmpty ? l10n.get('titleNotFilled') : _titleCtrl.text,
-                        style: TextStyle(
-                          fontSize: 20,
-                          fontWeight: FontWeight.w700,
-                          color: colors.textPrimary,
-                        ),
-                      ),
-                      const SizedBox(height: 12),
-                      Text(
-                        l10n.getString('typeAndPublishDate', args: {'type': _type}),
-                        style: TextStyle(
-                          fontSize: 13,
-                          color: colors.textSecondary,
-                        ),
-                      ),
-                      const SizedBox(height: 16),
-                      Container(
-                        width: double.infinity,
-                        height: 2,
-                        color: colors.danger,
-                      ),
-                      const SizedBox(height: 16),
-                      Text(
-                        _contentCtrl.text.isEmpty
-                            ? l10n.get('contentNotFilled')
-                            : _contentCtrl.text,
-                        style: TextStyle(
-                          fontSize: 14,
-                          color: colors.textSecondary,
-                          height: 1.7,
-                        ),
-                      ),
-                    ],
-                  ),
-                ),
-              ),
-            ],
-          ),
-        ),
-      ),
-    );
-  }
 
   void _confirmPublish() {
     final l10n = AppLocalizations.of(context);

+ 16 - 14
lib/features/expense/expense_apply_import_page.dart

@@ -577,22 +577,24 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
       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),
+    return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
+      Expanded(
+        child: 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(
+      SafeArea(
+        top: false, left: false, right: false,
         child: Padding(
-          padding: const EdgeInsets.all(12),
+          padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
           child: TDButton(
             text: l10n.get('confirmImport'),
             size: TDButtonSize.large,
@@ -601,6 +603,6 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
           ),
         ),
       ),
-    );
+    ]);
   }
 }

+ 1 - 2
lib/features/expense/expense_create_page.dart

@@ -185,11 +185,10 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage> {
     final controller = ref.watch(expenseCreateProvider(widget.editId).notifier);
     final state = ref.watch(expenseCreateProvider(widget.editId));
     final r = ResponsiveHelper.of(context);
-    final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final bottomInset = MediaQuery.of(context).padding.bottom;
 
-    if (_firstBuild) return const Scaffold(body: SkeletonFormPage(showImportLink: true));
+    if (_firstBuild) return const SkeletonFormPage(showImportLink: true);
 
     Future.microtask(() => ref.read(pageBackProvider.notifier).state = () => _doPop());
     Widget pageContent = PopScope(

+ 13 - 10
lib/features/expense/expense_list_page.dart

@@ -118,8 +118,8 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage> {
       });
     }
 
-    return Scaffold(
-      body: Column(children: [
+    return Stack(children: [
+      Column(children: [
         Container(
           decoration: BoxDecoration(
             color: colors.bgCard,
@@ -127,9 +127,9 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage> {
           ),
           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))),
+              Expanded(child: GestureDetector(behavior: HitTestBehavior.opaque, 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))),
+              Expanded(child: GestureDetector(behavior: HitTestBehavior.opaque, 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: [
@@ -159,13 +159,16 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage> {
         ),
         Expanded(child: Container(color: colors.bgPage, child: _firstBuild ? const Center(child: SkeletonLoadingList()) : _ExpenseListContent(refreshCtrl: _refreshCtrl, onRefresh: _doRefresh))),
       ]),
-      floatingActionButton: FloatingActionButton(
-        onPressed: () => context.push('/report/expense-detail'),
-        backgroundColor: colors.primary,
-        shape: const CircleBorder(),
-        child: const Icon(Icons.bar_chart, color: Colors.white),
+      Positioned(
+        right: 16, bottom: 16,
+        child: FloatingActionButton(
+          onPressed: () => context.push('/report/expense-detail'),
+          backgroundColor: colors.primary,
+          shape: const CircleBorder(),
+          child: const Icon(Icons.bar_chart, color: Colors.white),
+        ),
       ),
-    );
+    ]);
   }
 }
 

+ 4 - 2
lib/features/expense/widgets/expense_detail_dialog.dart

@@ -1,3 +1,5 @@
+// ignore_for_file: use_build_context_synchronously
+
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
@@ -161,10 +163,10 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
         _doEnsureVisible(node, attempt + 1, insets);
         return;
       }
+      final ctx = node.context;
+      if (ctx == null) 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,

+ 2 - 2
lib/features/expense_apply/expense_apply_create_page.dart

@@ -193,7 +193,7 @@ class _ExpenseApplyCreatePageState extends ConsumerState<ExpenseApplyCreatePage>
     final l10n = AppLocalizations.of(context);
 
     if (_firstBuild) {
-      return const Scaffold(body: SkeletonFormPage());
+      return const SkeletonFormPage();
     }
 
     Future.microtask(() => ref.read(pageBackProvider.notifier).state = () => _doPop());
@@ -788,7 +788,7 @@ class _ExpenseApplyCreatePageState extends ConsumerState<ExpenseApplyCreatePage>
 
   // ═══ 3. 附件上传 ═══
 
-  /// 深度转换 JSON 解析的 List<dynamic> 为 List<Map>,确保 TDCascader 类型匹配
+  // 深度转换 JSON 解析的 List<dynamic> 为 List<Map>,确保 TDCascader 类型匹配
   List<Map<String, dynamic>> _convertAcctTree(dynamic tree) {
     if (tree is! List) return [];
     return tree.map<Map<String, dynamic>>((e) {

+ 14 - 11
lib/features/expense_apply/expense_apply_list_page.dart

@@ -113,8 +113,8 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage> {
       });
     }
 
-    return Scaffold(
-      body: Column(children: [
+    return Stack(children: [
+      Column(children: [
         Container(
           decoration: BoxDecoration(
             color: colors.bgCard,
@@ -122,9 +122,9 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage> {
           ),
           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))),
+              Expanded(child: GestureDetector(behavior: HitTestBehavior.opaque, 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))),
+              Expanded(child: GestureDetector(behavior: HitTestBehavior.opaque, 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: [
@@ -154,13 +154,16 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage> {
         ),
         Expanded(child: Container(color: colors.bgPage, child: _firstBuild ? const Center(child: SkeletonLoadingList()) : _ExpenseApplyListContent(refreshCtrl: _refreshCtrl, onRefresh: _doRefresh))),
       ]),
-      floatingActionButton: FloatingActionButton(
-        onPressed: () => context.push('/report/expense-apply-detail'),
-        backgroundColor: colors.primary,
-        shape: const CircleBorder(),
-        child: const Icon(Icons.bar_chart, color: Colors.white),
+      Positioned(
+        right: 16, bottom: 16,
+        child: FloatingActionButton(
+          onPressed: () => context.push('/report/expense-apply-detail'),
+          backgroundColor: colors.primary,
+          shape: const CircleBorder(),
+          child: const Icon(Icons.bar_chart, color: Colors.white),
+        ),
       ),
-    );
+    ]);
   }
 }
 
@@ -173,7 +176,7 @@ class _ExpenseApplyListContent extends ConsumerWidget {
   Widget build(BuildContext context, WidgetRef ref) {
     final r = ResponsiveHelper.of(context);
     final itemsAsync = ref.watch(expenseApplyMyListProvider(''));
-    print('[TBOSS_OA][ExpenseApply] Content build: isLoading=${itemsAsync.isLoading}, hasValue=${itemsAsync.hasValue}, isReloading=${itemsAsync.isReloading}');
+    debugPrint('[TBOSS_OA][ExpenseApply] Content build: isLoading=${itemsAsync.isLoading}, hasValue=${itemsAsync.hasValue}, isReloading=${itemsAsync.isReloading}');
     if (itemsAsync.isLoading && !itemsAsync.hasValue) return Center(child: ConstrainedBox(constraints: BoxConstraints(maxWidth: r.listMaxWidth), child: const SkeletonLoadingList()));
     return Center(child: ConstrainedBox(constraints: BoxConstraints(maxWidth: r.listMaxWidth), child: EasyRefresh(controller: refreshCtrl, header: TDRefreshHeader(), onRefresh: onRefresh, child: _buildContent(itemsAsync, context, ref))));
   }

+ 4 - 2
lib/features/expense_apply/widgets/expense_apply_detail_dialog.dart

@@ -1,3 +1,5 @@
+// ignore_for_file: use_build_context_synchronously
+
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
@@ -200,10 +202,10 @@ class _ExpenseApplyDetailDialogState extends State<ExpenseApplyDetailDialog> {
       }
       // viewInsets 已稳定,但 AnimatedPadding 动画(200ms)可能还在进行
       // 等 250ms 确保 dialog 位置完全就位再滚动
+      final ctx = node.context;
+      if (ctx == null) 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,

+ 0 - 1
lib/features/messages/message_list_page.dart

@@ -27,7 +27,6 @@ class MessageListPage extends ConsumerWidget {
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final messagesAsync = ref.watch(messageListProvider);
     final l10n = AppLocalizations.of(context);
 

+ 21 - 25
lib/features/report/expense_apply_detail_report_page.dart

@@ -131,18 +131,16 @@ class _ExpenseApplyDetailReportPageState
       return const SkeletonReportPage();
     }
     if (_loading) {
-      return Scaffold(
-        body: SingleChildScrollView(
-          child: Column(children: [
-            _buildDateFilter(),
-            const SizedBox(height: 16),
-            Padding(padding: const EdgeInsets.symmetric(horizontal: 12), child: Bone(height: 80, borderRadius: BorderRadius.circular(12))),
-            const SizedBox(height: 16),
-            Padding(padding: const EdgeInsets.symmetric(horizontal: 12), child: Bone(height: 200, borderRadius: BorderRadius.circular(12))),
-            const SizedBox(height: 16),
-            Center(child: TDLoading(size: TDLoadingSize.medium, icon: TDLoadingIcon.activity)),
-          ]),
-        ),
+      return SingleChildScrollView(
+        child: Column(children: [
+          _buildDateFilter(),
+          const SizedBox(height: 16),
+          Padding(padding: const EdgeInsets.symmetric(horizontal: 12), child: Bone(height: 80, borderRadius: BorderRadius.circular(12))),
+          const SizedBox(height: 16),
+          Padding(padding: const EdgeInsets.symmetric(horizontal: 12), child: Bone(height: 200, borderRadius: BorderRadius.circular(12))),
+          const SizedBox(height: 16),
+          Center(child: TDLoading(size: TDLoadingSize.medium, icon: TDLoadingIcon.activity)),
+        ]),
       );
     }
     if (_error != null) {
@@ -154,17 +152,15 @@ class _ExpenseApplyDetailReportPageState
         ]),
       );
     }
-    return Scaffold(
-      body: SingleChildScrollView(
-        child: Column(
-          children: [
-            _buildDateFilter(),
-            _buildStatCards(),
-            _buildChartSection(),
-            _buildDetailList(),
-            const SizedBox(height: 80),
-          ],
-        ),
+    return SingleChildScrollView(
+      child: Column(
+        children: [
+          _buildDateFilter(),
+          _buildStatCards(),
+          _buildChartSection(),
+          _buildDetailList(),
+          const SizedBox(height: 80),
+        ],
       ),
     );
   }
@@ -182,11 +178,11 @@ class _ExpenseApplyDetailReportPageState
       child: Padding(
         padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
         child: Row(children: [
-          Expanded(child: GestureDetector(onTap: () => _pickDate(_startCtrl), child: _dateChip(_startCtrl, l10n.get('filterStartDate'), tdTheme, colors))),
+          Expanded(child: GestureDetector(behavior: HitTestBehavior.opaque, onTap: () => _pickDate(_startCtrl), child: _dateChip(_startCtrl, 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(_endCtrl), child: _dateChip(_endCtrl, l10n.get('filterEndDate'), tdTheme, colors))),
+          Expanded(child: GestureDetector(behavior: HitTestBehavior.opaque, onTap: () => _pickDate(_endCtrl), child: _dateChip(_endCtrl, l10n.get('filterEndDate'), tdTheme, colors))),
           const SizedBox(width: 8),
           GestureDetector(
             onTap: _applyFilter,

+ 21 - 25
lib/features/report/expense_detail_report_page.dart

@@ -146,11 +146,11 @@ class _ExpenseDetailReportPageState
       child: Padding(
         padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
         child: Row(children: [
-          Expanded(child: GestureDetector(onTap: () => _pickDate(_startCtrl), child: _dateChip(_startCtrl, l10n.get('filterStartDate'), tdTheme, colors))),
+          Expanded(child: GestureDetector(behavior: HitTestBehavior.opaque, onTap: () => _pickDate(_startCtrl), child: _dateChip(_startCtrl, 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(_endCtrl), child: _dateChip(_endCtrl, l10n.get('filterEndDate'), tdTheme, colors))),
+          Expanded(child: GestureDetector(behavior: HitTestBehavior.opaque, onTap: () => _pickDate(_endCtrl), child: _dateChip(_endCtrl, l10n.get('filterEndDate'), tdTheme, colors))),
           const SizedBox(width: 8),
           GestureDetector(
             onTap: () {
@@ -470,18 +470,16 @@ class _ExpenseDetailReportPageState
       return const SkeletonReportPage();
     }
     if (_loading) {
-      return Scaffold(
-        body: SingleChildScrollView(
-          child: Column(children: [
-            _buildDateFilter(),
-            const SizedBox(height: 16),
-            Padding(padding: const EdgeInsets.symmetric(horizontal: 12), child: Bone(height: 80, borderRadius: BorderRadius.circular(12))),
-            const SizedBox(height: 16),
-            Padding(padding: const EdgeInsets.symmetric(horizontal: 12), child: Bone(height: 200, borderRadius: BorderRadius.circular(12))),
-            const SizedBox(height: 16),
-            Center(child: TDLoading(size: TDLoadingSize.medium, icon: TDLoadingIcon.activity)),
-          ]),
-        ),
+      return SingleChildScrollView(
+        child: Column(children: [
+          _buildDateFilter(),
+          const SizedBox(height: 16),
+          Padding(padding: const EdgeInsets.symmetric(horizontal: 12), child: Bone(height: 80, borderRadius: BorderRadius.circular(12))),
+          const SizedBox(height: 16),
+          Padding(padding: const EdgeInsets.symmetric(horizontal: 12), child: Bone(height: 200, borderRadius: BorderRadius.circular(12))),
+          const SizedBox(height: 16),
+          Center(child: TDLoading(size: TDLoadingSize.medium, icon: TDLoadingIcon.activity)),
+        ]),
       );
     }
     if (_error != null) {
@@ -502,17 +500,15 @@ class _ExpenseDetailReportPageState
         ]),
       );
     }
-    return Scaffold(
-      body: SingleChildScrollView(
-        child: Column(
-          children: [
-            _buildDateFilter(),
-            _buildStatCards(),
-            _buildChartSection(),
-            _buildDetailList(),
-            const SizedBox(height: 80),
-          ],
-        ),
+    return SingleChildScrollView(
+      child: Column(
+        children: [
+          _buildDateFilter(),
+          _buildStatCards(),
+          _buildChartSection(),
+          _buildDetailList(),
+          const SizedBox(height: 80),
+        ],
       ),
     );
   }

+ 19 - 17
lib/features/report/outing_log_detail_report_page.dart

@@ -86,8 +86,8 @@ class _OutingLogDetailReportPageState extends ConsumerState<OutingLogDetailRepor
     final role = ref.watch(currentRoleProvider);
     final showDetail = role != 'employee';
     final showExport = role == 'finance' || role == 'admin';
-    return Scaffold(
-      body: SingleChildScrollView(
+    return Stack(children: [
+      SingleChildScrollView(
         child: Column(
           children: [
             _buildTimeFilter(),
@@ -99,21 +99,23 @@ class _OutingLogDetailReportPageState extends ConsumerState<OutingLogDetailRepor
           ],
         ),
       ),
-      floatingActionButton: showExport
-          ? FloatingActionButton.small(
-              onPressed: () {
-                ScaffoldMessenger.of(context).showSnackBar(
-                  SnackBar(
-                    content: Text(l10n.get('exportPlaceholder')),
-                    duration: const Duration(seconds: 2),
-                  ),
-                );
-              },
-              backgroundColor: colors.primary,
-              child: const Icon(Icons.download, color: Colors.white),
-            )
-          : null,
-    );
+      if (showExport)
+        Positioned(
+          right: 16, bottom: 16,
+          child: FloatingActionButton.small(
+            onPressed: () {
+              ScaffoldMessenger.of(context).showSnackBar(
+                SnackBar(
+                  content: Text(l10n.get('exportPlaceholder')),
+                  duration: const Duration(seconds: 2),
+                ),
+              );
+            },
+            backgroundColor: colors.primary,
+            child: const Icon(Icons.download, color: Colors.white),
+          ),
+        ),
+    ]);
   }
 
   Widget _buildTimeFilter() {

+ 19 - 17
lib/features/report/overtime_detail_report_page.dart

@@ -94,8 +94,8 @@ class _OvertimeDetailReportPageState
     final role = ref.watch(currentRoleProvider);
     final showDetail = role != 'employee';
     final showExport = role == 'finance' || role == 'admin';
-    return Scaffold(
-      body: SingleChildScrollView(
+    return Stack(children: [
+      SingleChildScrollView(
         child: Column(
           children: [
             _buildTimeFilter(),
@@ -107,21 +107,23 @@ class _OvertimeDetailReportPageState
           ],
         ),
       ),
-      floatingActionButton: showExport
-          ? FloatingActionButton.small(
-              onPressed: () {
-                ScaffoldMessenger.of(context).showSnackBar(
-                  SnackBar(
-                    content: Text(l10n.get('exportPlaceholder')),
-                    duration: const Duration(seconds: 2),
-                  ),
-                );
-              },
-              backgroundColor: colors.primary,
-              child: const Icon(Icons.download, color: Colors.white),
-            )
-          : null,
-    );
+      if (showExport)
+        Positioned(
+          right: 16, bottom: 16,
+          child: FloatingActionButton.small(
+            onPressed: () {
+              ScaffoldMessenger.of(context).showSnackBar(
+                SnackBar(
+                  content: Text(l10n.get('exportPlaceholder')),
+                  duration: const Duration(seconds: 2),
+                ),
+              );
+            },
+            backgroundColor: colors.primary,
+            child: const Icon(Icons.download, color: Colors.white),
+          ),
+        ),
+    ]);
   }
 
   Widget _buildTimeFilter() {

+ 19 - 17
lib/features/report/vehicle_detail_report_page.dart

@@ -84,8 +84,8 @@ class _VehicleDetailReportPageState
     final role = ref.watch(currentRoleProvider);
     final showDetail = role != 'employee';
     final showExport = role == 'finance' || role == 'admin';
-    return Scaffold(
-      body: SingleChildScrollView(
+    return Stack(children: [
+      SingleChildScrollView(
         child: Column(
           children: [
             _buildTimeFilter(),
@@ -97,21 +97,23 @@ class _VehicleDetailReportPageState
           ],
         ),
       ),
-      floatingActionButton: showExport
-          ? FloatingActionButton.small(
-              onPressed: () {
-                ScaffoldMessenger.of(context).showSnackBar(
-                  SnackBar(
-                    content: Text(l10n.get('exportPlaceholder')),
-                    duration: const Duration(seconds: 2),
-                  ),
-                );
-              },
-              backgroundColor: colors.primary,
-              child: const Icon(Icons.download, color: Colors.white),
-            )
-          : null,
-    );
+      if (showExport)
+        Positioned(
+          right: 16, bottom: 16,
+          child: FloatingActionButton.small(
+            onPressed: () {
+              ScaffoldMessenger.of(context).showSnackBar(
+                SnackBar(
+                  content: Text(l10n.get('exportPlaceholder')),
+                  duration: const Duration(seconds: 2),
+                ),
+              );
+            },
+            backgroundColor: colors.primary,
+            child: const Icon(Icons.download, color: Colors.white),
+          ),
+        ),
+    ]);
   }
 
   Widget _buildTimeFilter() {