Jelajahi Sumber

fix #19407 优化页面

chengc 1 Minggu lalu
induk
melakukan
08baea40aa

+ 12 - 0
assets/i18n/en.json

@@ -43,6 +43,18 @@
     "add": "Add",
     "close": "Close",
     "copy": "Copy",
+    "datePickerTitle": "Select Date Range",
+    "today": "Today",
+    "thisWeek": "This Week",
+    "last7Days": "Last 7 Days",
+    "thisMonth": "This Month",
+    "lastMonth": "Last Month",
+    "last30Days": "Last 30 Days",
+    "thisQuarter": "This Quarter",
+    "thisYear": "This Year",
+    "startDateLabel": "Start Date",
+    "endDateLabel": "End Date",
+    "selectedDateRange": "Selected: ",
     "retry": "Retry",
     "confirmAdd": "Confirm Add",
     "confirmEdit": "Confirm Edit",

+ 12 - 0
assets/i18n/zh_CN.json

@@ -40,6 +40,18 @@
     "add": "添加",
     "close": "关闭",
     "copy": "复制",
+    "datePickerTitle": "选择日期区间",
+    "today": "今天",
+    "thisWeek": "本周",
+    "last7Days": "近7天",
+    "thisMonth": "本月",
+    "lastMonth": "上月",
+    "last30Days": "近30天",
+    "thisQuarter": "本季",
+    "thisYear": "本年",
+    "startDateLabel": "起始日期",
+    "endDateLabel": "截止日期",
+    "selectedDateRange": "已选日期区间:",
     "retry": "重试",
     "confirmAdd": "确认添加",
     "confirmEdit": "确认修改",

+ 12 - 0
assets/i18n/zh_TW.json

@@ -43,6 +43,18 @@
     "add": "添加",
     "close": "關閉",
     "copy": "複製",
+    "datePickerTitle": "選擇日期區間",
+    "today": "今天",
+    "thisWeek": "本週",
+    "last7Days": "近7天",
+    "thisMonth": "本月",
+    "lastMonth": "上月",
+    "last30Days": "近30天",
+    "thisQuarter": "本季",
+    "thisYear": "本年",
+    "startDateLabel": "起始日期",
+    "endDateLabel": "截止日期",
+    "selectedDateRange": "已選日期區間:",
     "retry": "重試",
     "confirmAdd": "確認添加",
     "confirmEdit": "確認修改",

+ 46 - 130
lib/features/expense/expense_apply_import_page.dart

@@ -1,10 +1,11 @@
-import 'package:flutter/material.dart';
+import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import 'package:easy_refresh/easy_refresh.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../../core/utils/amount_utils.dart';
 import '../../core/theme/app_colors_extension.dart';
+import '../../shared/widgets/date_range_picker.dart';
 import '../../shared/widgets/empty_state.dart';
 import '../../shared/widgets/app_skeletons.dart';
 import '../../shared/widgets/list_footer.dart';
@@ -110,6 +111,7 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
   List<ImportableItem> _items = [];
   final Map<String, bool> _expandedGroups = {};
   bool _loading = false;
+  bool _filterLoading = false;
   bool _initialLoaded = false;
   bool _hasMore = true;
   int _page = 1;
@@ -200,11 +202,16 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
           _page = 2;
         }
         _loading = false;
+        _filterLoading = false;
         _initialLoaded = true;
         _hasMore = list.length >= 20;
       });
     } catch (_) {
-      if (mounted) setState(() => _loading = false);
+      if (mounted)
+        setState(() {
+          _loading = false;
+          _filterLoading = false;
+        });
     }
   }
 
@@ -219,7 +226,9 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
       );
       return;
     }
-    _refreshCtrl.callRefresh();
+    setState(() => _filterLoading = true);
+    _page = 1;
+    _load();
   }
 
   Future<void> _refresh() async {
@@ -264,50 +273,6 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
     Navigator.of(context).pop(selected);
   }
 
-  void _pickDate(TextEditingController ctrl) {
-    FocusManager.instance.primaryFocus?.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(
@@ -320,37 +285,13 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
         children: [
           Padding(
             padding: const EdgeInsets.fromLTRB(16, 8, 16, 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,
-                    ),
-                  ),
-                ),
-              ],
+            child: DateRangePicker(
+              startCtrl: _startDateCtrl,
+              endCtrl: _endDateCtrl,
+              onChanged: () {
+                setState(() {});
+                _search();
+              },
             ),
           ),
           const SizedBox(height: 8),
@@ -400,51 +341,6 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
     );
   }
 
-  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,
@@ -667,12 +563,16 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
             Container(
               width: 24,
               alignment: Alignment.center,
-              child: Text(
-                '#${d.itm}',
-                style: TextStyle(
-                  fontSize: 13,
-                  fontWeight: FontWeight.w500,
-                  color: colors.textSecondary,
+              child: FittedBox(
+                fit: BoxFit.scaleDown,
+                child: Text(
+                  '#${d.itm}',
+                  maxLines: 1,
+                  style: TextStyle(
+                    fontSize: 13,
+                    fontWeight: FontWeight.w500,
+                    color: colors.textSecondary,
+                  ),
                 ),
               ),
             ),
@@ -753,6 +653,7 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
                 ],
               ),
             ),
+            const SizedBox(width: 8),
             Text(
               formatAmount(d.amtnYj),
               style: TextStyle(
@@ -853,7 +754,22 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
               children: [
                 _buildSearchBar(l10n, colors),
                 const SizedBox(height: 8),
-                Expanded(child: _buildListContent(l10n, colors, grouped)),
+                Expanded(
+                  child: Stack(
+                    children: [
+                      _buildListContent(l10n, colors, grouped),
+                      if (_filterLoading) ...[
+                        Container(color: colors.bgPage.withValues(alpha: 0.6)),
+                        const Center(
+                          child: TDLoading(
+                            size: TDLoadingSize.medium,
+                            icon: TDLoadingIcon.activity,
+                          ),
+                        ),
+                      ],
+                    ],
+                  ),
+                ),
               ],
             ),
           ),

+ 70 - 128
lib/features/expense/expense_list_page.dart

@@ -1,4 +1,4 @@
-import 'package:flutter/material.dart';
+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';
@@ -6,6 +6,7 @@ import 'package:easy_refresh/easy_refresh.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../core/utils/responsive.dart';
+import '../../shared/widgets/date_range_picker.dart';
 import '../../shared/widgets/list_card.dart';
 import '../../shared/widgets/empty_state.dart';
 import '../../shared/widgets/app_skeletons.dart';
@@ -29,6 +30,7 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage> {
   late final EasyRefreshController _refreshCtrl;
   bool _firstBuild = true;
   bool _invalidateDone = false;
+  bool _filterLoading = false;
 
   final _scrollCtrl = ScrollController();
   bool _showBackToTop = false;
@@ -40,7 +42,7 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage> {
     _startDateCtrl.text =
         '${now.year}-${now.month.toString().padLeft(2, '0')}-01';
     _endDateCtrl.text =
-        '${now.year}-${now.month.toString().padLeft(2, '0')}-${_daysInMonth(now.year, now.month).toString().padLeft(2, '0')}';
+        '${now.year}-${now.month.toString().padLeft(2, '0')}-${DateTime(now.year, now.month + 1, 0).day.toString().padLeft(2, '0')}';
     _refreshCtrl = EasyRefreshController();
     _scrollCtrl.addListener(() {
       final show = _scrollCtrl.hasClients && _scrollCtrl.offset > 300;
@@ -57,7 +59,7 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage> {
       ref.read(expenseDateEndProvider.notifier).state = DateTime(
         now.year,
         now.month,
-        _daysInMonth(now.year, now.month),
+        DateTime(now.year, now.month + 1, 0).day,
       );
       ref.invalidate(expenseMyListProvider(''));
       _invalidateDone = true;
@@ -74,39 +76,10 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage> {
     super.dispose();
   }
 
-  void _pickDate(TextEditingController ctrl) {
-    FocusManager.instance.primaryFocus?.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) {
-        ctrl.text =
-            '${selected['year']}-${selected['month'].toString().padLeft(2, '0')}-${selected['day'].toString().padLeft(2, '0')}';
-        setState(() {});
-        Navigator.of(context).pop();
-      },
-    );
-  }
-
-  int _daysInMonth(int year, int month) => DateTime(year, month + 1, 0).day;
-
   void _applyFilter() {
     FocusManager.instance.primaryFocus?.unfocus();
-    _refreshCtrl.callRefresh();
+    setState(() => _filterLoading = true);
+    _doRefresh();
   }
 
   Future<void> _doRefresh() async {
@@ -124,51 +97,6 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage> {
     ref.read(expenseRefreshProvider.notifier).state++;
   }
 
-  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),
-            ),
-        ],
-      ),
-    );
-  }
-
   @override
   Widget build(BuildContext context) {
     final l10n = AppLocalizations.of(context);
@@ -183,6 +111,9 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage> {
             if (mounted) setState(() {});
           });
         }
+        if (_filterLoading && !next.isReloading && !next.isLoading) {
+          setState(() => _filterLoading = false);
+        }
       });
     }
 
@@ -202,42 +133,13 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage> {
                 children: [
                   Padding(
                     padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
-                    child: Row(
-                      children: [
-                        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(
-                            behavior: HitTestBehavior.opaque,
-                            onTap: () => _pickDate(_endDateCtrl),
-                            child: _dateChip(
-                              _endDateCtrl,
-                              l10n.get('filterEndDate'),
-                              tdTheme,
-                              colors,
-                            ),
-                          ),
-                        ),
-                      ],
+                    child: DateRangePicker(
+                      startCtrl: _startDateCtrl,
+                      endCtrl: _endDateCtrl,
+                      onChanged: () {
+                        setState(() {});
+                        _applyFilter();
+                      },
                     ),
                   ),
                   const SizedBox(height: 8),
@@ -292,13 +194,26 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage> {
             Expanded(
               child: Container(
                 color: colors.bgPage,
-                child: _firstBuild
-                    ? const Center(child: SkeletonLoadingList())
-                    : _ExpenseListContent(
-                        refreshCtrl: _refreshCtrl,
-                        onRefresh: _doRefresh,
-                        scrollCtrl: _scrollCtrl,
+                child: Stack(
+                  children: [
+                    _firstBuild
+                        ? const Center(child: SkeletonLoadingList())
+                        : _ExpenseListContent(
+                            refreshCtrl: _refreshCtrl,
+                            onRefresh: _doRefresh,
+                            scrollCtrl: _scrollCtrl,
+                          ),
+                    if (_filterLoading) ...[
+                      Container(color: colors.bgPage.withValues(alpha: 0.6)),
+                      const Center(
+                        child: TDLoading(
+                          size: TDLoadingSize.medium,
+                          icon: TDLoadingIcon.activity,
+                        ),
                       ),
+                    ],
+                  ],
+                ),
               ),
             ),
           ],
@@ -319,13 +234,24 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage> {
                 duration: const Duration(milliseconds: 200),
                 opacity: _showBackToTop ? 1.0 : 0.0,
                 child: Container(
-                  width: 36, height: 36,
+                  width: 36,
+                  height: 36,
                   decoration: BoxDecoration(
                     color: colors.primary400,
                     shape: BoxShape.circle,
-                    boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.15), blurRadius: 6, offset: const Offset(0, 2))],
+                    boxShadow: [
+                      BoxShadow(
+                        color: Colors.black.withValues(alpha: 0.15),
+                        blurRadius: 6,
+                        offset: const Offset(0, 2),
+                      ),
+                    ],
+                  ),
+                  child: const Icon(
+                    Icons.keyboard_arrow_up,
+                    color: Colors.white,
+                    size: 22,
                   ),
-                  child: const Icon(Icons.keyboard_arrow_up, color: Colors.white, size: 22),
                 ),
               ),
             ),
@@ -491,7 +417,11 @@ class _ExpenseListContent extends ConsumerWidget {
           ),
           child: Text(
             l10n.get('statusTransferred'),
-            style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.primary),
+            style: TextStyle(
+              fontSize: 11,
+              fontWeight: FontWeight.w500,
+              color: colors.primary,
+            ),
           ),
         ),
       );
@@ -506,7 +436,11 @@ class _ExpenseListContent extends ConsumerWidget {
           ),
           child: Text(
             l10n.get('statusClosed'),
-            style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.primary),
+            style: TextStyle(
+              fontSize: 11,
+              fontWeight: FontWeight.w500,
+              color: colors.primary,
+            ),
           ),
         ),
       );
@@ -522,7 +456,11 @@ class _ExpenseListContent extends ConsumerWidget {
             ),
             child: Text(
               l10n.get('statusApproved'),
-              style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.success),
+              style: TextStyle(
+                fontSize: 11,
+                fontWeight: FontWeight.w500,
+                color: colors.success,
+              ),
             ),
           ),
         );
@@ -536,7 +474,11 @@ class _ExpenseListContent extends ConsumerWidget {
             ),
             child: Text(
               l10n.get('statusPending'),
-              style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.statusGray),
+              style: TextStyle(
+                fontSize: 11,
+                fontWeight: FontWeight.w500,
+                color: colors.statusGray,
+              ),
             ),
           ),
         );

+ 364 - 166
lib/features/expense_apply/expense_apply_list_page.dart

@@ -1,4 +1,4 @@
-import 'package:flutter/material.dart';
+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';
@@ -6,6 +6,7 @@ import 'package:easy_refresh/easy_refresh.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../core/utils/responsive.dart';
+import '../../shared/widgets/date_range_picker.dart';
 import '../../shared/widgets/list_card.dart';
 import '../../shared/widgets/empty_state.dart';
 import '../../shared/widgets/app_skeletons.dart';
@@ -15,10 +16,12 @@ import 'expense_apply_list_controller.dart';
 import 'expense_apply_model.dart';
 import 'expense_apply_approval_api.dart';
 import '../../core/utils/amount_utils.dart';
+
 class ExpenseApplyListPage extends ConsumerStatefulWidget {
   const ExpenseApplyListPage({super.key});
   @override
-  ConsumerState<ExpenseApplyListPage> createState() => _ExpenseApplyListPageState();
+  ConsumerState<ExpenseApplyListPage> createState() =>
+      _ExpenseApplyListPageState();
 }
 
 class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage> {
@@ -28,6 +31,7 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage> {
   late final EasyRefreshController _refreshCtrl;
   bool _firstBuild = true;
   bool _invalidateDone = false;
+  bool _filterLoading = false;
 
   final _scrollCtrl = ScrollController();
   bool _showBackToTop = false;
@@ -36,8 +40,10 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage> {
   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')}';
+    _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')}';
     _refreshCtrl = EasyRefreshController();
     _scrollCtrl.addListener(() {
       final show = _scrollCtrl.hasClients && _scrollCtrl.offset > 300;
@@ -46,8 +52,16 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage> {
       }
     });
     WidgetsBinding.instance.addPostFrameCallback((_) {
-      ref.read(expenseApplyDateStartProvider.notifier).state = DateTime(now.year, now.month, 1);
-      ref.read(expenseApplyDateEndProvider.notifier).state = DateTime(now.year, now.month, DateTime(now.year, now.month + 1, 0).day);
+      ref.read(expenseApplyDateStartProvider.notifier).state = DateTime(
+        now.year,
+        now.month,
+        1,
+      );
+      ref.read(expenseApplyDateEndProvider.notifier).state = DateTime(
+        now.year,
+        now.month,
+        DateTime(now.year, now.month + 1, 0).day,
+      );
       ref.invalidate(expenseApplyMyListProvider(''));
       _invalidateDone = true;
       setState(() {}); // 触发重建以挂载 ref.listen
@@ -63,48 +77,27 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage> {
     super.dispose();
   }
 
-  void _pickDate(TextEditingController ctrl) {
-    FocusManager.instance.primaryFocus?.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) {
-        ctrl.text = '${selected['year']}-${selected['month'].toString().padLeft(2, '0')}-${selected['day'].toString().padLeft(2, '0')}';
-        setState(() {});
-        Navigator.of(context).pop();
-      },
-    );
-  }
-
   void _applyFilter() {
     FocusManager.instance.primaryFocus?.unfocus();
-    _refreshCtrl.callRefresh();
+    setState(() => _filterLoading = true);
+    _doRefresh();
   }
 
   Future<void> _doRefresh() async {
     ref.read(expenseApplyKeywordProvider.notifier).state = _keyword;
-    ref.read(expenseApplyDateStartProvider.notifier).state = _startDateCtrl.text.isNotEmpty ? DateTime.tryParse(_startDateCtrl.text) : null;
-    ref.read(expenseApplyDateEndProvider.notifier).state = _endDateCtrl.text.isNotEmpty ? DateTime.tryParse(_endDateCtrl.text) : null;
+    ref
+        .read(expenseApplyDateStartProvider.notifier)
+        .state = _startDateCtrl.text.isNotEmpty
+        ? DateTime.tryParse(_startDateCtrl.text)
+        : null;
+    ref
+        .read(expenseApplyDateEndProvider.notifier)
+        .state = _endDateCtrl.text.isNotEmpty
+        ? DateTime.tryParse(_endDateCtrl.text)
+        : null;
     ref.read(expenseApplyRefreshProvider.notifier).state++;
   }
 
-  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)),
-      ]),
-    );
-  }
-
   @override
   Widget build(BuildContext context) {
     final l10n = AppLocalizations.of(context);
@@ -120,88 +113,167 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage> {
             if (mounted) setState(() {});
           });
         }
+        if (_filterLoading && !next.isReloading && !next.isLoading) {
+          setState(() => _filterLoading = false);
+        }
       });
     }
 
-    return Stack(children: [
-      Column(children: [
-        Container(
-          decoration: BoxDecoration(
-            color: colors.bgCard,
-            border: Border(bottom: BorderSide(color: tdTheme.componentStrokeColor)),
-          ),
-          child: Column(mainAxisSize: MainAxisSize.min, children: [
-            Padding(padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), child: Row(children: [
-              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(behavior: HitTestBehavior.opaque, onTap: () => _pickDate(_endDateCtrl), child: _dateChip(_endDateCtrl, l10n.get('filterEndDate'), tdTheme, colors))),
-            ])),
-            const SizedBox(height: 8),
-            Padding(padding: const EdgeInsets.fromLTRB(0, 0, 12, 8), child: Row(children: [
-              Expanded(
-                child: TDSearchBar(
-                  placeHolder: l10n.get('searchExpenseApply'),
-                  style: TDSearchStyle.round,
-                  onTextChanged: (String text) {
-                    setState(() {
-                      _keyword = text;
-                    });
-                    if (text.isEmpty) _applyFilter();
-                  },
-                  onSubmitted: (_) => _applyFilter(),
+    return Stack(
+      children: [
+        Column(
+          children: [
+            Container(
+              decoration: BoxDecoration(
+                color: colors.bgCard,
+                border: Border(
+                  bottom: BorderSide(color: tdTheme.componentStrokeColor),
                 ),
               ),
-              GestureDetector(
-                onTap: () {
-                  final dir = ref.read(expenseApplySortDirProvider);
-                  ref.read(expenseApplySortDirProvider.notifier).state = dir == 'ASC' ? 'DESC' : 'ASC';
-                  _applyFilter();
-                },
-                child: Container(width: 40, height: 40, decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(20)), child: Center(child: Icon(ref.watch(expenseApplySortDirProvider) == 'ASC' ? Icons.arrow_upward : Icons.arrow_downward, color: Colors.white, size: 20))),
+              child: Column(
+                mainAxisSize: MainAxisSize.min,
+                children: [
+                  Padding(
+                    padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
+                    child: DateRangePicker(
+                      startCtrl: _startDateCtrl,
+                      endCtrl: _endDateCtrl,
+                      onChanged: () {
+                        setState(() {});
+                        _applyFilter();
+                      },
+                    ),
+                  ),
+                  const SizedBox(height: 8),
+                  Padding(
+                    padding: const EdgeInsets.fromLTRB(0, 0, 12, 8),
+                    child: Row(
+                      children: [
+                        Expanded(
+                          child: TDSearchBar(
+                            placeHolder: l10n.get('searchExpenseApply'),
+                            style: TDSearchStyle.round,
+                            onTextChanged: (String text) {
+                              setState(() {
+                                _keyword = text;
+                              });
+                              if (text.isEmpty) _applyFilter();
+                            },
+                            onSubmitted: (_) => _applyFilter(),
+                          ),
+                        ),
+                        GestureDetector(
+                          onTap: () {
+                            final dir = ref.read(expenseApplySortDirProvider);
+                            ref
+                                .read(expenseApplySortDirProvider.notifier)
+                                .state = dir == 'ASC'
+                                ? 'DESC'
+                                : 'ASC';
+                            _applyFilter();
+                          },
+                          child: Container(
+                            width: 40,
+                            height: 40,
+                            decoration: BoxDecoration(
+                              color: colors.primary,
+                              borderRadius: BorderRadius.circular(20),
+                            ),
+                            child: Center(
+                              child: Icon(
+                                ref.watch(expenseApplySortDirProvider) == 'ASC'
+                                    ? Icons.arrow_upward
+                                    : Icons.arrow_downward,
+                                color: Colors.white,
+                                size: 20,
+                              ),
+                            ),
+                          ),
+                        ),
+                      ],
+                    ),
+                  ),
+                ],
               ),
-            ])),
-          ]),
-        ),
-        Expanded(child: Container(color: colors.bgPage, child: _firstBuild ? const Center(child: SkeletonLoadingList()) : _ExpenseApplyListContent(refreshCtrl: _refreshCtrl, onRefresh: _doRefresh, scrollCtrl: _scrollCtrl))),
-      ]),
-      AnimatedPositioned(
-        duration: const Duration(milliseconds: 200),
-        bottom: _showBackToTop ? 80 : -60,
-        left: 0,
-        right: 0,
-        child: Center(
-          child: GestureDetector(
-            onTap: () {
-              if (_scrollCtrl.hasClients) {
-                _scrollCtrl.jumpTo(0);
-              }
-            },
-            child: AnimatedOpacity(
-              duration: const Duration(milliseconds: 200),
-              opacity: _showBackToTop ? 1.0 : 0.0,
+            ),
+            Expanded(
               child: Container(
-                width: 36, height: 36,
-                decoration: BoxDecoration(
-                  color: colors.primary400,
-                  shape: BoxShape.circle,
-                  boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.15), blurRadius: 6, offset: const Offset(0, 2))],
+                color: colors.bgPage,
+                child: Stack(
+                  children: [
+                    _firstBuild
+                        ? const Center(child: SkeletonLoadingList())
+                        : _ExpenseApplyListContent(
+                            refreshCtrl: _refreshCtrl,
+                            onRefresh: _doRefresh,
+                            scrollCtrl: _scrollCtrl,
+                          ),
+                    if (_filterLoading) ...[
+                      Container(color: colors.bgPage.withValues(alpha: 0.6)),
+                      const Center(
+                        child: TDLoading(
+                          size: TDLoadingSize.medium,
+                          icon: TDLoadingIcon.activity,
+                        ),
+                      ),
+                    ],
+                  ],
+                ),
+              ),
+            ),
+          ],
+        ),
+        AnimatedPositioned(
+          duration: const Duration(milliseconds: 200),
+          bottom: _showBackToTop ? 80 : -60,
+          left: 0,
+          right: 0,
+          child: Center(
+            child: GestureDetector(
+              onTap: () {
+                if (_scrollCtrl.hasClients) {
+                  _scrollCtrl.jumpTo(0);
+                }
+              },
+              child: AnimatedOpacity(
+                duration: const Duration(milliseconds: 200),
+                opacity: _showBackToTop ? 1.0 : 0.0,
+                child: Container(
+                  width: 36,
+                  height: 36,
+                  decoration: BoxDecoration(
+                    color: colors.primary400,
+                    shape: BoxShape.circle,
+                    boxShadow: [
+                      BoxShadow(
+                        color: Colors.black.withValues(alpha: 0.15),
+                        blurRadius: 6,
+                        offset: const Offset(0, 2),
+                      ),
+                    ],
+                  ),
+                  child: const Icon(
+                    Icons.keyboard_arrow_up,
+                    color: Colors.white,
+                    size: 22,
+                  ),
                 ),
-                child: const Icon(Icons.keyboard_arrow_up, color: Colors.white, size: 22),
               ),
             ),
           ),
         ),
-      ),
-      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),
+        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),
+          ),
         ),
-      ),
-    ]);
+      ],
+    );
   }
 }
 
@@ -209,77 +281,184 @@ class _ExpenseApplyListContent extends ConsumerWidget {
   final EasyRefreshController refreshCtrl;
   final Future<void> Function() onRefresh;
   final ScrollController scrollCtrl;
-  const _ExpenseApplyListContent({required this.refreshCtrl, required this.onRefresh, required this.scrollCtrl});
+  const _ExpenseApplyListContent({
+    required this.refreshCtrl,
+    required this.onRefresh,
+    required this.scrollCtrl,
+  });
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
     final r = ResponsiveHelper.of(context);
     final itemsAsync = ref.watch(expenseApplyMyListProvider(''));
-    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))));
+    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),
+        ),
+      ),
+    );
   }
 
-  Widget _buildContent(AsyncValue<List<ExpenseApplyModel>> itemsAsync, BuildContext context, WidgetRef ref) {
+  Widget _buildContent(
+    AsyncValue<List<ExpenseApplyModel>> itemsAsync,
+    BuildContext context,
+    WidgetRef ref,
+  ) {
     final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     if (itemsAsync.isReloading) {
       final oldItems = itemsAsync.valueOrNull ?? [];
-      if (oldItems.isEmpty) return ListView(children: [const SizedBox(height: 120), EmptyState(message: l10n.get('noExpenseApplications'))]);
-      return ListView.builder(controller: scrollCtrl, padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), itemCount: oldItems.length, itemBuilder: (_, i) {
-        final m = oldItems[i];
-        final card = ListCard(
-          cardNo: m.expenseApplyNo, amount: formatAmount(m.estimatedAmount),
-          applicant: m.deptName.isNotEmpty ? '${m.applicantName} · ${m.deptName}' : m.applicantName,
-          description: m.remark.isNotEmpty ? '${m.purpose}\n${l10n.get('remark')}: ${m.remark}' : m.purpose,
-          date: du.DateUtils.formatDate(m.createTime),
-          statusTag: Row(mainAxisSize: MainAxisSize.min, children: [
-            if (m.isTransferred == 1)
-              Container(
-                padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
-                decoration: BoxDecoration(color: colors.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4)),
-                child: Text(l10n.get('statusConvertedToExpense'), style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.primary)),
-              ),
-            if (m.isTransferred == 1) const SizedBox(width: 6),
-            if (m.isTransferred != 1) _buildAuditTag(m, l10n, colors),
-            if (m.isTransferred != 1) const SizedBox(width: 6),
-            _buildUrgencyChip(m.urgency, l10n, colors),
-          ]),
-          onTap: () { context.push('/expense-apply/detail/${m.expenseApplyNo}'); },
+      if (oldItems.isEmpty)
+        return ListView(
+          children: [
+            const SizedBox(height: 120),
+            EmptyState(message: l10n.get('noExpenseApplications')),
+          ],
         );
-        return Padding(padding: const EdgeInsets.only(bottom: 16), child: card);
-      });
+      return ListView.builder(
+        controller: scrollCtrl,
+        padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
+        itemCount: oldItems.length,
+        itemBuilder: (_, i) {
+          final m = oldItems[i];
+          final card = ListCard(
+            cardNo: m.expenseApplyNo,
+            amount: formatAmount(m.estimatedAmount),
+            applicant: m.deptName.isNotEmpty
+                ? '${m.applicantName} · ${m.deptName}'
+                : m.applicantName,
+            description: m.remark.isNotEmpty
+                ? '${m.purpose}\n${l10n.get('remark')}: ${m.remark}'
+                : m.purpose,
+            date: du.DateUtils.formatDate(m.createTime),
+            statusTag: Row(
+              mainAxisSize: MainAxisSize.min,
+              children: [
+                if (m.isTransferred == 1)
+                  Container(
+                    padding: const EdgeInsets.symmetric(
+                      horizontal: 8,
+                      vertical: 2,
+                    ),
+                    decoration: BoxDecoration(
+                      color: colors.primary.withValues(alpha: 0.1),
+                      borderRadius: BorderRadius.circular(4),
+                    ),
+                    child: Text(
+                      l10n.get('statusConvertedToExpense'),
+                      style: TextStyle(
+                        fontSize: 11,
+                        fontWeight: FontWeight.w500,
+                        color: colors.primary,
+                      ),
+                    ),
+                  ),
+                if (m.isTransferred == 1) const SizedBox(width: 6),
+                if (m.isTransferred != 1) _buildAuditTag(m, l10n, colors),
+                if (m.isTransferred != 1) const SizedBox(width: 6),
+                _buildUrgencyChip(m.urgency, l10n, colors),
+              ],
+            ),
+            onTap: () {
+              context.push('/expense-apply/detail/${m.expenseApplyNo}');
+            },
+          );
+          return Padding(
+            padding: const EdgeInsets.only(bottom: 16),
+            child: card,
+          );
+        },
+      );
     }
-    if (itemsAsync.hasError) return ListView(children: [const SizedBox(height: 120), EmptyState(message: l10n.get('loadFailed'))]);
+    if (itemsAsync.hasError)
+      return ListView(
+        children: [
+          const SizedBox(height: 120),
+          EmptyState(message: l10n.get('loadFailed')),
+        ],
+      );
     final items = itemsAsync.requireValue;
-    if (items.isEmpty) return ListView(children: [const SizedBox(height: 120), EmptyState(message: l10n.get('noExpenseApplications'))]);
-    return ListView.builder(controller: scrollCtrl, padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), itemCount: items.length + 1, itemBuilder: (_, i) {
-      if (i == items.length) return ListFooter(itemCount: items.length);
-      final m = items[i];
-      final card = ListCard(
-        cardNo: m.expenseApplyNo, amount: formatAmount(m.estimatedAmount),
-        applicant: m.deptName.isNotEmpty ? '${m.applicantName} · ${m.deptName}' : m.applicantName,
-        description: m.remark.isNotEmpty ? '${m.purpose}\n${l10n.get('remark')}: ${m.remark}' : m.purpose,
-        date: du.DateUtils.formatDate(m.createTime),
-        statusTag: Row(mainAxisSize: MainAxisSize.min, children: [
-          if (m.isTransferred == 1)
-            Container(
-              padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
-              decoration: BoxDecoration(color: colors.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4)),
-              child: Text(l10n.get('statusConvertedToExpense'), style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.primary)),
-            ),
-          if (m.isTransferred == 1) const SizedBox(width: 6),
-          if (m.isTransferred != 1) _buildAuditTag(m, l10n, colors),
-          if (m.isTransferred != 1) const SizedBox(width: 6),
-          _buildUrgencyChip(m.urgency, l10n, colors),
-        ]),
-        onTap: () { context.push('/expense-apply/detail/${m.expenseApplyNo}'); },
+    if (items.isEmpty)
+      return ListView(
+        children: [
+          const SizedBox(height: 120),
+          EmptyState(message: l10n.get('noExpenseApplications')),
+        ],
       );
-      return Padding(padding: const EdgeInsets.only(bottom: 16), child: card);
-    });
+    return ListView.builder(
+      controller: scrollCtrl,
+      padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
+      itemCount: items.length + 1,
+      itemBuilder: (_, i) {
+        if (i == items.length) return ListFooter(itemCount: items.length);
+        final m = items[i];
+        final card = ListCard(
+          cardNo: m.expenseApplyNo,
+          amount: formatAmount(m.estimatedAmount),
+          applicant: m.deptName.isNotEmpty
+              ? '${m.applicantName} · ${m.deptName}'
+              : m.applicantName,
+          description: m.remark.isNotEmpty
+              ? '${m.purpose}\n${l10n.get('remark')}: ${m.remark}'
+              : m.purpose,
+          date: du.DateUtils.formatDate(m.createTime),
+          statusTag: Row(
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              if (m.isTransferred == 1)
+                Container(
+                  padding: const EdgeInsets.symmetric(
+                    horizontal: 8,
+                    vertical: 2,
+                  ),
+                  decoration: BoxDecoration(
+                    color: colors.primary.withValues(alpha: 0.1),
+                    borderRadius: BorderRadius.circular(4),
+                  ),
+                  child: Text(
+                    l10n.get('statusConvertedToExpense'),
+                    style: TextStyle(
+                      fontSize: 11,
+                      fontWeight: FontWeight.w500,
+                      color: colors.primary,
+                    ),
+                  ),
+                ),
+              if (m.isTransferred == 1) const SizedBox(width: 6),
+              if (m.isTransferred != 1) _buildAuditTag(m, l10n, colors),
+              if (m.isTransferred != 1) const SizedBox(width: 6),
+              _buildUrgencyChip(m.urgency, l10n, colors),
+            ],
+          ),
+          onTap: () {
+            context.push('/expense-apply/detail/${m.expenseApplyNo}');
+          },
+        );
+        return Padding(padding: const EdgeInsets.only(bottom: 16), child: card);
+      },
+    );
   }
 
-  Widget _buildUrgencyChip(String urgency, AppLocalizations l10n, AppColorsExtension colors) {
+  Widget _buildUrgencyChip(
+    String urgency,
+    AppLocalizations l10n,
+    AppColorsExtension colors,
+  ) {
     // 与费用申请单创建页保持一致: critical→danger, urgent→warning, normal→primary
     final (label, color) = switch (urgency) {
       '3' || 'critical' => (l10n.get('critical'), colors.danger),
@@ -293,11 +472,22 @@ class _ExpenseApplyListContent extends ConsumerWidget {
         borderRadius: BorderRadius.circular(4),
         border: Border.all(color: color, width: 0.5),
       ),
-      child: Text(label, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: color)),
+      child: Text(
+        label,
+        style: TextStyle(
+          fontSize: 11,
+          fontWeight: FontWeight.w500,
+          color: color,
+        ),
+      ),
     );
   }
 
-  Widget _buildAuditTag(ExpenseApplyModel item, AppLocalizations l10n, AppColorsExtension colors) {
+  Widget _buildAuditTag(
+    ExpenseApplyModel item,
+    AppLocalizations l10n,
+    AppColorsExtension colors,
+  ) {
     if (item.isAuditApproved) {
       return Container(
         padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
@@ -307,7 +497,11 @@ class _ExpenseApplyListContent extends ConsumerWidget {
         ),
         child: Text(
           l10n.get('statusApproved'),
-          style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.success),
+          style: TextStyle(
+            fontSize: 11,
+            fontWeight: FontWeight.w500,
+            color: colors.success,
+          ),
         ),
       );
     }
@@ -319,7 +513,11 @@ class _ExpenseApplyListContent extends ConsumerWidget {
       ),
       child: Text(
         l10n.get('statusPending'),
-        style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.statusGray),
+        style: TextStyle(
+          fontSize: 11,
+          fontWeight: FontWeight.w500,
+          color: colors.statusGray,
+        ),
       ),
     );
   }

+ 123 - 195
lib/features/report/expense_apply_detail_report_page.dart

@@ -5,10 +5,12 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../shared/widgets/app_skeletons.dart';
+import '../../shared/widgets/date_range_picker.dart';
 import 'package:skeletonizer/skeletonizer.dart';
 import '../../shared/widgets/list_footer.dart';
 import '../expense_apply/expense_apply_api.dart';
 import '../expense_apply/report_model.dart';
+import 'package:marquee/marquee.dart';
 import '../../core/utils/amount_utils.dart';
 
 /// 事前申请明细报表 - 页面22
@@ -26,6 +28,7 @@ class _ExpenseApplyDetailReportPageState
   final _endCtrl = TextEditingController();
   bool _loading = true;
   bool _isInitialLoad = true;
+  bool _filterLoading = false;
   String? _error;
   ReportData? _data;
   List<ReportDetailItem> _details = [];
@@ -59,11 +62,15 @@ class _ExpenseApplyDetailReportPageState
     super.dispose();
   }
 
-  Future<void> _loadData() async {
+  Future<void> _loadData({bool isRefresh = false}) async {
     _activeStartDate = _startCtrl.text;
     _activeEndDate = _endCtrl.text;
     setState(() {
-      _loading = true;
+      if (isRefresh) {
+        _filterLoading = true;
+      } else {
+        _loading = true;
+      }
       _error = null;
     });
     try {
@@ -76,6 +83,7 @@ class _ExpenseApplyDetailReportPageState
         setState(() {
           _data = data;
           _loading = false;
+          _filterLoading = false;
         });
         _loadDetails();
       }
@@ -84,6 +92,7 @@ class _ExpenseApplyDetailReportPageState
         setState(() {
           _error = e.toString();
           _loading = false;
+          _filterLoading = false;
         });
       }
     }
@@ -154,97 +163,49 @@ class _ExpenseApplyDetailReportPageState
     }
   }
 
-  void _pickDate(TextEditingController ctrl) {
-    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) {
-        ctrl.text =
-            '${selected['year']}-${selected['month'].toString().padLeft(2, '0')}-${selected['day'].toString().padLeft(2, '0')}';
-        setState(() {});
-        Navigator.of(context).pop();
-      },
-    );
-  }
-
-  bool _validateDateRange() {
-    final l10n = AppLocalizations.of(context);
-    if (_startCtrl.text.isEmpty || _endCtrl.text.isEmpty) {
-      TDToast.showText(l10n.get('selectDateRange'), context: context);
-      return false;
-    }
-    final start = DateTime.tryParse(_activeStartDate);
-    final end = DateTime.tryParse(_activeEndDate);
-    if (start == null || end == null) {
-      TDToast.showText(l10n.get('selectDateRange'), context: context);
-      return false;
-    }
-    if (end.isBefore(start)) {
-      TDToast.showText(l10n.get('endDateBeforeStart'), context: context);
-      return false;
-    }
-    if (end.difference(start).inDays > 366) {
-      TDToast.showText(l10n.get('dateRangeLimit'), context: context);
-      return false;
-    }
-    return true;
-  }
-
-  void _applyFilter() {
-    if (!_validateDateRange()) return;
-    _details = [];
-    _detailPage = 1;
-    _detailTotal = 0;
-    _isInitialLoad = false;
-    _loadData();
-    _loadDetails();
-  }
-
   @override
   Widget build(BuildContext context) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
     if (_loading && _isInitialLoad) {
-      return const SkeletonReportPage();
+      return const SkeletonExpenseApplyDetailReportPage();
     }
     if (_loading) {
-      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,
+      return Column(
+        children: [
+          _buildDateFilter(),
+          Expanded(
+            child: SingleChildScrollView(
+              child: Column(
+                children: [
+                  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) {
@@ -263,17 +224,36 @@ class _ExpenseApplyDetailReportPageState
         ),
       );
     }
-    return SingleChildScrollView(
-      controller: _scrollCtrl,
-      child: Column(
-        children: [
-          _buildDateFilter(),
-          _buildStatCards(),
-          _buildChartSection(),
-          _buildDetailList(),
-          const SizedBox(height: 80),
+    return Stack(
+      children: [
+        Column(
+          children: [
+            _buildDateFilter(),
+            Expanded(
+              child: SingleChildScrollView(
+                controller: _scrollCtrl,
+                child: Column(
+                  children: [
+                    _buildStatCards(),
+                    _buildChartSection(),
+                    _buildDetailList(),
+                    const SizedBox(height: 80),
+                  ],
+                ),
+              ),
+            ),
+          ],
+        ),
+        if (_filterLoading) ...[
+          Container(color: colors.bgPage.withValues(alpha: 0.6)),
+          const Center(
+            child: TDLoading(
+              size: TDLoadingSize.medium,
+              icon: TDLoadingIcon.activity,
+            ),
+          ),
         ],
-      ),
+      ],
     );
   }
 
@@ -281,110 +261,30 @@ class _ExpenseApplyDetailReportPageState
   Widget _buildDateFilter() {
     final tdTheme = TDTheme.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
-    final l10n = AppLocalizations.of(context);
     return Container(
       decoration: BoxDecoration(
         color: colors.bgCard,
         border: Border(bottom: BorderSide(color: tdTheme.componentStrokeColor)),
       ),
       child: Padding(
-        padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
-        child: Row(
-          children: [
-            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(
-                behavior: HitTestBehavior.opaque,
-                onTap: () => _pickDate(_endCtrl),
-                child: _dateChip(
-                  _endCtrl,
-                  l10n.get('filterEndDate'),
-                  tdTheme,
-                  colors,
-                ),
-              ),
-            ),
-            const SizedBox(width: 8),
-            GestureDetector(
-              onTap: _applyFilter,
-              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),
-              ),
-            ),
-          ],
+        padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
+        child: DateRangePicker(
+          startCtrl: _startCtrl,
+          endCtrl: _endCtrl,
+          onChanged: () {
+            setState(() {});
+            _details = [];
+            _detailPage = 1;
+            _detailTotal = 0;
+            _isInitialLoad = false;
+            _loadData(isRefresh: true);
+            _loadDetails();
+          },
         ),
       ),
     );
   }
 
-  Widget _dateChip(
-    TextEditingController ctrl,
-    String hint,
-    TDThemeData tdTheme,
-    AppColorsExtension colors,
-  ) {
-    final text = ctrl.text;
-    return Container(
-      height: 40,
-      padding: const EdgeInsets.symmetric(horizontal: 8),
-      decoration: BoxDecoration(
-        color: colors.bgSecondaryContainer,
-        borderRadius: BorderRadius.circular(20),
-        border: Border.all(color: tdTheme.componentStrokeColor),
-      ),
-      child: Row(
-        children: [
-          Icon(Icons.calendar_today, size: 14, color: colors.textSecondary),
-          const SizedBox(width: 4),
-          Expanded(
-            child: Text(
-              text.isNotEmpty ? text : hint,
-              maxLines: 1,
-              overflow: TextOverflow.ellipsis,
-              style: TextStyle(
-                fontSize: 12,
-                color: text.isNotEmpty
-                    ? colors.textPrimary
-                    : colors.textSecondary,
-              ),
-            ),
-          ),
-          if (text.isNotEmpty)
-            GestureDetector(
-              onTap: () {
-                ctrl.clear();
-                setState(() {});
-              },
-              child: Icon(Icons.close, size: 16, color: colors.textSecondary),
-            ),
-        ],
-      ),
-    );
-  }
-
   // ── 2 张统计卡片(一行两列)──
   Widget _buildStatCards() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
@@ -431,17 +331,12 @@ class _ExpenseApplyDetailReportPageState
       ),
       child: Column(
         children: [
-          Text(
-            value,
-            style: TextStyle(
-              fontSize: 18,
-              fontWeight: FontWeight.w700,
-              color: valueColor,
-            ),
-          ),
+          _buildValue(value, valueColor),
           const SizedBox(height: 4),
           Text(
             label,
+            maxLines: 1,
+            overflow: TextOverflow.ellipsis,
             style: TextStyle(fontSize: 12, color: colors.textSecondary),
           ),
         ],
@@ -449,6 +344,39 @@ class _ExpenseApplyDetailReportPageState
     );
   }
 
+  Widget _buildValue(String value, Color valueColor) {
+    return LayoutBuilder(
+      builder: (context, constraints) {
+        final style = TextStyle(
+          fontSize: 18,
+          fontWeight: FontWeight.w700,
+          color: valueColor,
+        );
+        final tp = TextPainter(
+          text: TextSpan(text: value, style: style),
+          textDirection: TextDirection.ltr,
+          textScaler: MediaQuery.textScalerOf(context),
+          maxLines: 1,
+        )..layout();
+        if (tp.width > constraints.maxWidth) {
+          return SizedBox(
+            height: tp.height,
+            child: Marquee(
+              text: value,
+              style: style,
+              scrollAxis: Axis.horizontal,
+              blankSpace: 40,
+              velocity: 30,
+              pauseAfterRound: const Duration(seconds: 1),
+              startPadding: 0,
+            ),
+          );
+        }
+        return Text(value, softWrap: false, style: style);
+      },
+    );
+  }
+
   // ── 图表区域 ──
   Widget _buildChartSection() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;

+ 125 - 197
lib/features/report/expense_detail_report_page.dart

@@ -5,10 +5,12 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../shared/widgets/app_skeletons.dart';
+import '../../shared/widgets/date_range_picker.dart';
 import 'package:skeletonizer/skeletonizer.dart';
 import '../../shared/widgets/list_footer.dart';
 import '../expense/expense_api.dart';
 import '../expense_apply/report_model.dart';
+import 'package:marquee/marquee.dart';
 import '../../core/utils/amount_utils.dart';
 
 /// 费用报销明细报表 - 页面23
@@ -26,6 +28,7 @@ class _ExpenseDetailReportPageState
   final _endCtrl = TextEditingController();
   bool _loading = true;
   bool _isInitialLoad = true;
+  bool _filterLoading = false;
   String? _error;
   ReportData? _data;
   List<ReportDetailItem> _details = [];
@@ -61,11 +64,15 @@ class _ExpenseDetailReportPageState
     super.dispose();
   }
 
-  Future<void> _loadData() async {
+  Future<void> _loadData({bool isRefresh = false}) async {
     _activeStartDate = _startCtrl.text;
     _activeEndDate = _endCtrl.text;
     setState(() {
-      _loading = true;
+      if (isRefresh) {
+        _filterLoading = true;
+      } else {
+        _loading = true;
+      }
       _error = null;
     });
     try {
@@ -78,6 +85,7 @@ class _ExpenseDetailReportPageState
         setState(() {
           _data = data;
           _loading = false;
+          _filterLoading = false;
         });
         _loadDetails();
         _loadSubordinateData();
@@ -87,6 +95,7 @@ class _ExpenseDetailReportPageState
         setState(() {
           _error = e.toString();
           _loading = false;
+          _filterLoading = false;
         });
       }
     }
@@ -176,107 +185,9 @@ class _ExpenseDetailReportPageState
     }
   }
 
-  bool _validateDateRange() {
-    final l10n = AppLocalizations.of(context);
-    if (_startCtrl.text.isEmpty || _endCtrl.text.isEmpty) {
-      TDToast.showText(l10n.get('selectDateRange'), context: context);
-      return false;
-    }
-    final start = DateTime.tryParse(_activeStartDate);
-    final end = DateTime.tryParse(_activeEndDate);
-    if (start == null || end == null) {
-      TDToast.showText(l10n.get('selectDateRange'), context: context);
-      return false;
-    }
-    if (end.isBefore(start)) {
-      TDToast.showText(l10n.get('endDateBeforeStart'), context: context);
-      return false;
-    }
-    if (end.difference(start).inDays > 366) {
-      TDToast.showText(l10n.get('dateRangeLimit'), context: context);
-      return false;
-    }
-    return true;
-  }
-
-  // ── 日期选择 ──
-  void _pickDate(TextEditingController ctrl) {
-    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) {
-        ctrl.text =
-            '${selected['year']}-${selected['month'].toString().padLeft(2, '0')}-${selected['day'].toString().padLeft(2, '0')}';
-        setState(() {});
-        Navigator.of(context).pop();
-      },
-    );
-  }
-
-  // ── 日期 chip ──
-  Widget _dateChip(
-    TextEditingController ctrl,
-    String hint,
-    TDThemeData tdTheme,
-    AppColorsExtension colors,
-  ) {
-    final text = ctrl.text;
-    return Container(
-      height: 40,
-      padding: const EdgeInsets.symmetric(horizontal: 8),
-      decoration: BoxDecoration(
-        color: colors.bgSecondaryContainer,
-        borderRadius: BorderRadius.circular(20),
-        border: Border.all(color: tdTheme.componentStrokeColor),
-      ),
-      child: Row(
-        children: [
-          Icon(Icons.calendar_today, size: 14, color: colors.textSecondary),
-          const SizedBox(width: 4),
-          Expanded(
-            child: Text(
-              text.isNotEmpty ? text : hint,
-              maxLines: 1,
-              overflow: TextOverflow.ellipsis,
-              style: TextStyle(
-                fontSize: 12,
-                color: text.isNotEmpty
-                    ? colors.textPrimary
-                    : colors.textSecondary,
-              ),
-            ),
-          ),
-          if (text.isNotEmpty)
-            GestureDetector(
-              onTap: () {
-                ctrl.clear();
-                setState(() {});
-              },
-              child: Icon(Icons.close, size: 16, color: colors.textSecondary),
-            ),
-        ],
-      ),
-    );
-  }
-
   // ── 日期过滤区 ──
   Widget _buildDateFilter() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
-    final l10n = AppLocalizations.of(context);
     final tdTheme = TDTheme.of(context);
     return Container(
       decoration: BoxDecoration(
@@ -284,61 +195,19 @@ class _ExpenseDetailReportPageState
         border: Border(bottom: BorderSide(color: tdTheme.componentStrokeColor)),
       ),
       child: Padding(
-        padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
-        child: Row(
-          children: [
-            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(
-                behavior: HitTestBehavior.opaque,
-                onTap: () => _pickDate(_endCtrl),
-                child: _dateChip(
-                  _endCtrl,
-                  l10n.get('filterEndDate'),
-                  tdTheme,
-                  colors,
-                ),
-              ),
-            ),
-            const SizedBox(width: 8),
-            GestureDetector(
-              onTap: () {
-                if (!_validateDateRange()) return;
-                _details = [];
-                _detailPage = 1;
-                _detailTotal = 0;
-                _isInitialLoad = false;
-                _loadData();
-                _loadDetails();
-              },
-              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),
-              ),
-            ),
-          ],
+        padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
+        child: DateRangePicker(
+          startCtrl: _startCtrl,
+          endCtrl: _endCtrl,
+          onChanged: () {
+            setState(() {});
+            _details = [];
+            _detailPage = 1;
+            _detailTotal = 0;
+            _isInitialLoad = false;
+            _loadData(isRefresh: true);
+            _loadDetails();
+          },
         ),
       ),
     );
@@ -401,17 +270,12 @@ class _ExpenseDetailReportPageState
       ),
       child: Column(
         children: [
-          Text(
-            value,
-            style: TextStyle(
-              fontSize: 18,
-              fontWeight: FontWeight.w700,
-              color: valueColor,
-            ),
-          ),
+          _buildValue(value, valueColor),
           const SizedBox(height: 4),
           Text(
             label,
+            maxLines: 1,
+            overflow: TextOverflow.ellipsis,
             style: TextStyle(fontSize: 12, color: colors.textSecondary),
           ),
         ],
@@ -419,6 +283,39 @@ class _ExpenseDetailReportPageState
     );
   }
 
+  Widget _buildValue(String value, Color valueColor) {
+    return LayoutBuilder(
+      builder: (context, constraints) {
+        final style = TextStyle(
+          fontSize: 18,
+          fontWeight: FontWeight.w700,
+          color: valueColor,
+        );
+        final tp = TextPainter(
+          text: TextSpan(text: value, style: style),
+          textDirection: TextDirection.ltr,
+          textScaler: MediaQuery.textScalerOf(context),
+          maxLines: 1,
+        )..layout();
+        if (tp.width > constraints.maxWidth) {
+          return SizedBox(
+            height: tp.height,
+            child: Marquee(
+              text: value,
+              style: style,
+              scrollAxis: Axis.horizontal,
+              blankSpace: 40,
+              velocity: 30,
+              pauseAfterRound: const Duration(seconds: 1),
+              startPadding: 0,
+            ),
+          );
+        }
+        return Text(value, softWrap: false, style: style);
+      },
+    );
+  }
+
   // ── 图表 ──
   Widget _buildChartSection() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
@@ -857,32 +754,44 @@ class _ExpenseDetailReportPageState
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
     if (_loading && _isInitialLoad) {
-      return const SkeletonReportPage();
+      return const SkeletonExpenseDetailReportPage();
     }
     if (_loading) {
-      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,
+      return Column(
+        children: [
+          _buildDateFilter(),
+          Expanded(
+            child: SingleChildScrollView(
+              child: Column(
+                children: [
+                  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) {
@@ -919,19 +828,38 @@ class _ExpenseDetailReportPageState
         ),
       );
     }
-    return SingleChildScrollView(
-      controller: _scrollCtrl,
-      child: Column(
-        children: [
-          _buildDateFilter(),
-          _buildStatCards(),
-          _buildChartSection(),
-          _buildBarChartSection(),
-          const SizedBox(height: 12),
-          _buildDetailList(),
-          const SizedBox(height: 80),
+    return Stack(
+      children: [
+        Column(
+          children: [
+            _buildDateFilter(),
+            Expanded(
+              child: SingleChildScrollView(
+                controller: _scrollCtrl,
+                child: Column(
+                  children: [
+                    _buildStatCards(),
+                    _buildChartSection(),
+                    _buildBarChartSection(),
+                    const SizedBox(height: 12),
+                    _buildDetailList(),
+                    const SizedBox(height: 80),
+                  ],
+                ),
+              ),
+            ),
+          ],
+        ),
+        if (_filterLoading) ...[
+          Container(color: colors.bgPage.withValues(alpha: 0.6)),
+          const Center(
+            child: TDLoading(
+              size: TDLoadingSize.medium,
+              icon: TDLoadingIcon.activity,
+            ),
+          ),
         ],
-      ),
+      ],
     );
   }
 

+ 86 - 50
lib/features/report/outing_log_detail_report_page.dart

@@ -1,4 +1,5 @@
 import 'package:flutter/material.dart';
+import 'package:marquee/marquee.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:fl_chart/fl_chart.dart';
 import '../../core/i18n/app_localizations.dart';
@@ -14,7 +15,8 @@ class OutingLogDetailReportPage extends ConsumerStatefulWidget {
       _OutingLogDetailReportPageState();
 }
 
-class _OutingLogDetailReportPageState extends ConsumerState<OutingLogDetailReportPage> {
+class _OutingLogDetailReportPageState
+    extends ConsumerState<OutingLogDetailReportPage> {
   int _timeFilterIdx = 0;
   DateTime? _customStart;
   DateTime? _customEnd;
@@ -86,36 +88,39 @@ class _OutingLogDetailReportPageState extends ConsumerState<OutingLogDetailRepor
     final role = ref.watch(currentRoleProvider);
     final showDetail = role != 'employee';
     final showExport = role == 'finance' || role == 'admin';
-    return Stack(children: [
-      SingleChildScrollView(
-        child: Column(
-          children: [
-            _buildTimeFilter(),
-            _buildCustomerSearch(),
-            _buildStatCards(),
-            _buildChartSection(),
-            if (showDetail) _buildDeptListSection(),
-            const SizedBox(height: 80),
-          ],
-        ),
-      ),
-      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),
+    return Stack(
+      children: [
+        SingleChildScrollView(
+          child: Column(
+            children: [
+              _buildTimeFilter(),
+              _buildCustomerSearch(),
+              _buildStatCards(),
+              _buildChartSection(),
+              if (showDetail) _buildDeptListSection(),
+              const SizedBox(height: 80),
+            ],
           ),
         ),
-    ]);
+        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() {
@@ -333,27 +338,12 @@ class _OutingLogDetailReportPageState extends ConsumerState<OutingLogDetailRepor
       ),
       child: Column(
         children: [
-          Row(
-            mainAxisAlignment: MainAxisAlignment.center,
-            mainAxisSize: MainAxisSize.min,
-            children: [
-              if (showStar) ...[
-                Icon(Icons.star, size: 16, color: colors.warning),
-                const SizedBox(width: 2),
-              ],
-              Text(
-                value,
-                style: TextStyle(
-                  fontSize: 18,
-                  fontWeight: FontWeight.w700,
-                  color: valueColor,
-                ),
-              ),
-            ],
-          ),
+          _buildValueWithStar(value, valueColor, showStar),
           const SizedBox(height: 4),
           Text(
             label,
+            maxLines: 1,
+            overflow: TextOverflow.ellipsis,
             style: TextStyle(fontSize: 12, color: colors.textSecondary),
           ),
         ],
@@ -362,6 +352,54 @@ class _OutingLogDetailReportPageState extends ConsumerState<OutingLogDetailRepor
   }
 
   // ── 图表 ──
+  Widget _buildValueWithStar(String value, Color valueColor, bool showStar) {
+    final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    return LayoutBuilder(
+      builder: (context, constraints) {
+        final style = TextStyle(
+          fontSize: 18,
+          fontWeight: FontWeight.w700,
+          color: valueColor,
+        );
+        // 计算星标占用宽度
+        const starWidth = 16.0 + 2.0; // icon + SizedBox
+        final availableWidth = constraints.maxWidth;
+        final tp = TextPainter(
+          text: TextSpan(text: value, style: style),
+          textDirection: TextDirection.ltr,
+          textScaler: MediaQuery.textScalerOf(context),
+          maxLines: 1,
+        )..layout();
+        final totalWidth = tp.width + (showStar ? starWidth : 0);
+        if (totalWidth > availableWidth) {
+          return SizedBox(
+            height: tp.height,
+            child: Marquee(
+              text: value,
+              style: style,
+              scrollAxis: Axis.horizontal,
+              blankSpace: 40,
+              velocity: 30,
+              pauseAfterRound: const Duration(seconds: 1),
+              startPadding: 0,
+            ),
+          );
+        }
+        return Row(
+          mainAxisAlignment: MainAxisAlignment.center,
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            if (showStar) ...[
+              Icon(Icons.star, size: 16, color: colors.warning),
+              const SizedBox(width: 2),
+            ],
+            Text(value, softWrap: false, style: style),
+          ],
+        );
+      },
+    );
+  }
+
   Widget _buildChartSection() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
@@ -716,9 +754,7 @@ class _OutingLogDetailReportPageState extends ConsumerState<OutingLogDetailRepor
             ),
             Container(
               padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
-              decoration: BoxDecoration(
-                color: colors.bgDisabled,
-              ),
+              decoration: BoxDecoration(color: colors.bgDisabled),
               child: Row(
                 children: [
                   SizedBox(

+ 69 - 39
lib/features/report/overtime_detail_report_page.dart

@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:fl_chart/fl_chart.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../../core/theme/app_colors_extension.dart';
+import 'package:marquee/marquee.dart';
 import '../../core/auth/role_provider.dart';
 
 /// 加班明细报表 - 页面24
@@ -94,36 +95,39 @@ class _OvertimeDetailReportPageState
     final role = ref.watch(currentRoleProvider);
     final showDetail = role != 'employee';
     final showExport = role == 'finance' || role == 'admin';
-    return Stack(children: [
-      SingleChildScrollView(
-        child: Column(
-          children: [
-            _buildTimeFilter(),
-            _buildTypeFilter(),
-            _buildStatCards(),
-            _buildChartSection(),
-            if (showDetail) _buildDeptListSection(),
-            const SizedBox(height: 80),
-          ],
-        ),
-      ),
-      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),
+    return Stack(
+      children: [
+        SingleChildScrollView(
+          child: Column(
+            children: [
+              _buildTimeFilter(),
+              _buildTypeFilter(),
+              _buildStatCards(),
+              _buildChartSection(),
+              if (showDetail) _buildDeptListSection(),
+              const SizedBox(height: 80),
+            ],
           ),
         ),
-    ]);
+        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() {
@@ -334,17 +338,12 @@ class _OvertimeDetailReportPageState
       ),
       child: Column(
         children: [
-          Text(
-            value,
-            style: TextStyle(
-              fontSize: 18,
-              fontWeight: FontWeight.w700,
-              color: valueColor,
-            ),
-          ),
+          _buildValue(value, valueColor),
           const SizedBox(height: 4),
           Text(
             label,
+            maxLines: 1,
+            overflow: TextOverflow.ellipsis,
             style: TextStyle(fontSize: 12, color: colors.textSecondary),
           ),
         ],
@@ -353,6 +352,39 @@ class _OvertimeDetailReportPageState
   }
 
   // ── 图表:堆叠柱状图 ──
+  Widget _buildValue(String value, Color valueColor) {
+    return LayoutBuilder(
+      builder: (context, constraints) {
+        final style = TextStyle(
+          fontSize: 18,
+          fontWeight: FontWeight.w700,
+          color: valueColor,
+        );
+        final tp = TextPainter(
+          text: TextSpan(text: value, style: style),
+          textDirection: TextDirection.ltr,
+          textScaler: MediaQuery.textScalerOf(context),
+          maxLines: 1,
+        )..layout();
+        if (tp.width > constraints.maxWidth) {
+          return SizedBox(
+            height: tp.height,
+            child: Marquee(
+              text: value,
+              style: style,
+              scrollAxis: Axis.horizontal,
+              blankSpace: 40,
+              velocity: 30,
+              pauseAfterRound: const Duration(seconds: 1),
+              startPadding: 0,
+            ),
+          );
+        }
+        return Text(value, softWrap: false, style: style);
+      },
+    );
+  }
+
   Widget _buildChartSection() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
@@ -668,9 +700,7 @@ class _OvertimeDetailReportPageState
             ),
             Container(
               padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
-              decoration: BoxDecoration(
-                color: colors.bgDisabled,
-              ),
+              decoration: BoxDecoration(color: colors.bgDisabled),
               child: Row(
                 children: [
                   SizedBox(

+ 70 - 43
lib/features/report/vehicle_detail_report_page.dart

@@ -4,6 +4,7 @@ import 'package:fl_chart/fl_chart.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/auth/role_provider.dart';
+import 'package:marquee/marquee.dart';
 import '../../core/utils/amount_utils.dart';
 
 /// 用车明细报表 - 页面25
@@ -85,36 +86,39 @@ class _VehicleDetailReportPageState
     final role = ref.watch(currentRoleProvider);
     final showDetail = role != 'employee';
     final showExport = role == 'finance' || role == 'admin';
-    return Stack(children: [
-      SingleChildScrollView(
-        child: Column(
-          children: [
-            _buildTimeFilter(),
-            _buildExtraFilters(),
-            _buildStatCards(),
-            _buildChartSection(),
-            if (showDetail) _buildDeptListSection(),
-            const SizedBox(height: 80),
-          ],
-        ),
-      ),
-      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),
+    return Stack(
+      children: [
+        SingleChildScrollView(
+          child: Column(
+            children: [
+              _buildTimeFilter(),
+              _buildExtraFilters(),
+              _buildStatCards(),
+              _buildChartSection(),
+              if (showDetail) _buildDeptListSection(),
+              const SizedBox(height: 80),
+            ],
           ),
         ),
-    ]);
+        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() {
@@ -208,10 +212,7 @@ class _VehicleDetailReportPageState
   Widget _buildExtraFilters() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
-    final vehicleLabels = [
-      l10n.get('filterAll'),
-      ..._vehiclePlateFilters,
-    ];
+    final vehicleLabels = [l10n.get('filterAll'), ..._vehiclePlateFilters];
     final usageLabels = [
       l10n.get('filterAll'),
       l10n.get('filterReception'),
@@ -376,17 +377,12 @@ class _VehicleDetailReportPageState
       ),
       child: Column(
         children: [
-          Text(
-            value,
-            style: TextStyle(
-              fontSize: 18,
-              fontWeight: FontWeight.w700,
-              color: valueColor,
-            ),
-          ),
+          _buildValue(value, valueColor),
           const SizedBox(height: 4),
           Text(
             label,
+            maxLines: 1,
+            overflow: TextOverflow.ellipsis,
             style: TextStyle(fontSize: 12, color: colors.textSecondary),
           ),
         ],
@@ -395,6 +391,39 @@ class _VehicleDetailReportPageState
   }
 
   // ── 图表:双轴折线 ──
+  Widget _buildValue(String value, Color valueColor) {
+    return LayoutBuilder(
+      builder: (context, constraints) {
+        final style = TextStyle(
+          fontSize: 18,
+          fontWeight: FontWeight.w700,
+          color: valueColor,
+        );
+        final tp = TextPainter(
+          text: TextSpan(text: value, style: style),
+          textDirection: TextDirection.ltr,
+          textScaler: MediaQuery.textScalerOf(context),
+          maxLines: 1,
+        )..layout();
+        if (tp.width > constraints.maxWidth) {
+          return SizedBox(
+            height: tp.height,
+            child: Marquee(
+              text: value,
+              style: style,
+              scrollAxis: Axis.horizontal,
+              blankSpace: 40,
+              velocity: 30,
+              pauseAfterRound: const Duration(seconds: 1),
+              startPadding: 0,
+            ),
+          );
+        }
+        return Text(value, softWrap: false, style: style);
+      },
+    );
+  }
+
   Widget _buildChartSection() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
@@ -750,9 +779,7 @@ class _VehicleDetailReportPageState
             ),
             Container(
               padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
-              decoration: BoxDecoration(
-                color: colors.bgDisabled,
-              ),
+              decoration: BoxDecoration(color: colors.bgDisabled),
               child: Row(
                 children: [
                   SizedBox(

+ 381 - 109
lib/shared/widgets/app_skeletons.dart

@@ -57,10 +57,7 @@ class SkeletonListCard extends StatelessWidget {
           children: [
             Row(
               mainAxisAlignment: MainAxisAlignment.spaceBetween,
-              children: [
-                Bone.text(width: 120),
-                Bone.text(width: 80),
-              ],
+              children: [Bone.text(width: 120), Bone.text(width: 80)],
             ),
             SizedBox(height: 6),
             Bone.text(width: 140),
@@ -71,7 +68,11 @@ class SkeletonListCard extends StatelessWidget {
               mainAxisAlignment: MainAxisAlignment.spaceBetween,
               children: [
                 Bone.text(width: 100),
-                Bone(width: 48, height: 20, borderRadius: BorderRadius.circular(4)),
+                Bone(
+                  width: 48,
+                  height: 20,
+                  borderRadius: BorderRadius.circular(4),
+                ),
               ],
             ),
           ],
@@ -103,7 +104,11 @@ class SkeletonVehicleCard extends StatelessWidget {
               mainAxisAlignment: MainAxisAlignment.spaceBetween,
               children: [
                 Bone.text(width: 100),
-                Bone(width: 48, height: 20, borderRadius: BorderRadius.circular(4)),
+                Bone(
+                  width: 48,
+                  height: 20,
+                  borderRadius: BorderRadius.circular(4),
+                ),
               ],
             ),
             SizedBox(height: 6),
@@ -111,7 +116,11 @@ class SkeletonVehicleCard extends StatelessWidget {
               mainAxisAlignment: MainAxisAlignment.spaceBetween,
               children: [
                 Bone.text(width: 140),
-                Bone(width: 40, height: 16, borderRadius: BorderRadius.circular(4)),
+                Bone(
+                  width: 40,
+                  height: 16,
+                  borderRadius: BorderRadius.circular(4),
+                ),
               ],
             ),
             SizedBox(height: 6),
@@ -155,7 +164,11 @@ class SkeletonOutingLogCard extends StatelessWidget {
               children: [
                 Expanded(flex: 3, child: Bone.text()),
                 const SizedBox(width: 8),
-                Bone(width: 48, height: 20, borderRadius: BorderRadius.circular(4)),
+                Bone(
+                  width: 48,
+                  height: 20,
+                  borderRadius: BorderRadius.circular(4),
+                ),
               ],
             ),
             SizedBox(height: 4),
@@ -198,7 +211,11 @@ class SkeletonAnnouncementCard extends StatelessWidget {
             SizedBox(height: 8),
             Row(
               children: [
-                Bone(width: 56, height: 20, borderRadius: BorderRadius.circular(4)),
+                Bone(
+                  width: 56,
+                  height: 20,
+                  borderRadius: BorderRadius.circular(4),
+                ),
                 const SizedBox(width: 8),
                 Bone.text(width: 60),
                 const Spacer(),
@@ -254,7 +271,12 @@ class SkeletonImportCard extends StatelessWidget {
               ],
             ),
             SizedBox(height: 8),
-            Row(children: [const SizedBox(width: 54), Expanded(flex: 2, child: Bone.text())]),
+            Row(
+              children: [
+                const SizedBox(width: 54),
+                Expanded(flex: 2, child: Bone.text()),
+              ],
+            ),
           ],
         ),
       ),
@@ -358,16 +380,24 @@ class SkeletonFormPage extends StatelessWidget {
                 if (showImportLink) ...[
                   // 导入申请单按钮骨架
                   Container(
-                    padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
+                    padding: const EdgeInsets.symmetric(
+                      horizontal: 12,
+                      vertical: 10,
+                    ),
                     decoration: BoxDecoration(
                       color: colors.bgCard,
                       borderRadius: BorderRadius.circular(12),
                     ),
-                    child: Row(children: [
-                      Bone.square(size: 20, borderRadius: BorderRadius.circular(4)),
-                      const SizedBox(width: 8),
-                      Bone(width: 160, height: 14),
-                    ]),
+                    child: Row(
+                      children: [
+                        Bone.square(
+                          size: 20,
+                          borderRadius: BorderRadius.circular(4),
+                        ),
+                        const SizedBox(width: 8),
+                        Bone(width: 160, height: 14),
+                      ],
+                    ),
                   ),
                   const SizedBox(height: 16),
                 ],
@@ -384,7 +414,9 @@ class SkeletonFormPage extends StatelessWidget {
         ),
         // 底部操作栏骨架,贴底并适配安全区
         Positioned(
-          left: 0, right: 0, bottom: 0,
+          left: 0,
+          right: 0,
+          bottom: 0,
           child: Skeletonizer(
             enabled: true,
             child: Container(
@@ -395,9 +427,19 @@ class SkeletonFormPage extends StatelessWidget {
               ),
               child: Row(
                 children: [
-                  Expanded(child: Bone(height: 44, borderRadius: BorderRadius.circular(8))),
+                  Expanded(
+                    child: Bone(
+                      height: 44,
+                      borderRadius: BorderRadius.circular(8),
+                    ),
+                  ),
                   const SizedBox(width: 12),
-                  Expanded(child: Bone(height: 44, borderRadius: BorderRadius.circular(8))),
+                  Expanded(
+                    child: Bone(
+                      height: 44,
+                      borderRadius: BorderRadius.circular(8),
+                    ),
+                  ),
                 ],
               ),
             ),
@@ -424,115 +466,345 @@ class _FormCard extends StatelessWidget {
       child: Column(
         crossAxisAlignment: CrossAxisAlignment.start,
         children: [
-          Row(children: [
-            Bone.square(size: 20, borderRadius: BorderRadius.circular(4)),
-            const SizedBox(width: 8),
-            Bone(width: 80, height: 16),
-            const Spacer(),
-            Bone(width: 40, height: 16),
-          ]),
+          Row(
+            children: [
+              Bone.square(size: 20, borderRadius: BorderRadius.circular(4)),
+              const SizedBox(width: 8),
+              Bone(width: 80, height: 16),
+              const Spacer(),
+              Bone(width: 40, height: 16),
+            ],
+          ),
           const SizedBox(height: 20),
-          ...List.generate(rows, (i) => Padding(
-            padding: EdgeInsets.only(bottom: i < rows - 1 ? 16 : 0),
-            child: Row(
-              mainAxisAlignment: MainAxisAlignment.spaceBetween,
-              children: [
-                Bone(width: 64, height: 14),
-                Bone(width: 140 + (i % 3) * 20, height: 14),
-              ],
+          ...List.generate(
+            rows,
+            (i) => Padding(
+              padding: EdgeInsets.only(bottom: i < rows - 1 ? 16 : 0),
+              child: Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                children: [
+                  Bone(width: 64, height: 14),
+                  Bone(width: 140 + (i % 3) * 20, height: 14),
+                ],
+              ),
             ),
-          )),
+          ),
         ],
       ),
     );
   }
 }
 
-/// 报表页骨架(ExpenseDetailReportPage / ExpenseApplyDetailReportPage)
-/// 匹配 筛选栏 + 统计卡片(2x2) + 图表卡片 + 明细列表 布局
-class SkeletonReportPage extends StatelessWidget {
-  const SkeletonReportPage({super.key});
+// ═══ 报表页骨架 — 各页面按实际布局独立定义 ═══
+
+/// 事前申请明细报表骨架(ExpenseApplyDetailReportPage)
+/// 布局:筛选栏 + 统计卡片(1行2个) + 折线图(单线) + 明细列表
+class SkeletonExpenseApplyDetailReportPage extends StatelessWidget {
+  const SkeletonExpenseApplyDetailReportPage({super.key});
 
   @override
   Widget build(BuildContext context) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
-    return SingleChildScrollView(
-      child: Skeletonizer(
-        enabled: true,
-        child: Column(
-          children: [
-            // 日期筛选栏
-            Container(
-              padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
-              color: colors.bgCard,
-              child: Row(children: [
-                Expanded(child: Bone(height: 40, borderRadius: BorderRadius.circular(20))),
-                const SizedBox(width: 8),
-                Bone(width: 14, height: 14),
-                const SizedBox(width: 8),
-                Expanded(child: Bone(height: 40, borderRadius: BorderRadius.circular(20))),
-                const SizedBox(width: 8),
-                Bone(width: 40, height: 40, borderRadius: BorderRadius.circular(20)),
-              ]),
-            ),
-            const SizedBox(height: 16),
-            // 统计卡片 — 2行 x 2列
-            Padding(
-              padding: const EdgeInsets.symmetric(horizontal: 12),
-              child: Column(children: [
-                Row(children: [
-                  Expanded(child: Bone(height: 80, borderRadius: BorderRadius.circular(12))),
-                  const SizedBox(width: 8),
-                  Expanded(child: Bone(height: 80, borderRadius: BorderRadius.circular(12))),
-                ]),
-                const SizedBox(height: 8),
-                Row(children: [
-                  Expanded(child: Bone(height: 80, borderRadius: BorderRadius.circular(12))),
-                  const SizedBox(width: 8),
-                  Expanded(child: Bone(height: 80, borderRadius: BorderRadius.circular(12))),
-                ]),
-              ]),
-            ),
-            const SizedBox(height: 16),
-            // 图表卡片 — 标题 + 图表区
-            Padding(
-              padding: const EdgeInsets.symmetric(horizontal: 12),
-              child: Container(
-                padding: const EdgeInsets.all(16),
-                decoration: BoxDecoration(color: colors.bgCard, borderRadius: BorderRadius.circular(12)),
-                child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
-                  Row(children: [Bone.square(size: 20, borderRadius: BorderRadius.circular(4)), const SizedBox(width: 8), Bone(width: 120, height: 16)]),
+    return Column(
+      children: [
+        _dateFilterChip(colors),
+        Expanded(
+          child: SingleChildScrollView(
+            child: Skeletonizer(
+              enabled: true,
+              child: Column(
+                children: [
+                  // 统计卡片 — 1行2列
+                  Padding(
+                    padding: const EdgeInsets.fromLTRB(12, 12, 12, 0),
+                    child: Row(
+                      children: [
+                        Expanded(
+                          child: Bone(
+                            height: 80,
+                            borderRadius: BorderRadius.circular(8),
+                          ),
+                        ),
+                        const SizedBox(width: 8),
+                        Expanded(
+                          child: Bone(
+                            height: 80,
+                            borderRadius: BorderRadius.circular(8),
+                          ),
+                        ),
+                      ],
+                    ),
+                  ),
+                  const SizedBox(height: 16),
+                  // 折线图 — 标题 + 1个图例 + 图表区
+                  Padding(
+                    padding: const EdgeInsets.symmetric(horizontal: 12),
+                    child: Container(
+                      padding: const EdgeInsets.all(16),
+                      decoration: BoxDecoration(
+                        color: colors.bgCard,
+                        borderRadius: BorderRadius.circular(8),
+                      ),
+                      child: Column(
+                        crossAxisAlignment: CrossAxisAlignment.start,
+                        children: [
+                          Bone(width: 120, height: 16),
+                          const SizedBox(height: 8),
+                          Row(
+                            children: [
+                              Bone.square(
+                                size: 8,
+                                borderRadius: BorderRadius.circular(4),
+                              ),
+                              const SizedBox(width: 4),
+                              Bone(width: 60, height: 12),
+                            ],
+                          ),
+                          const SizedBox(height: 16),
+                          Row(
+                            children: [
+                              Expanded(
+                                child: Bone(
+                                  height: 200,
+                                  borderRadius: BorderRadius.circular(8),
+                                ),
+                              ),
+                            ],
+                          ),
+                        ],
+                      ),
+                    ),
+                  ),
                   const SizedBox(height: 16),
-                  Bone(height: 200, borderRadius: BorderRadius.circular(8)),
-                ]),
+                  // 明细列表
+                  _detailListSkeleton(colors),
+                  const SizedBox(height: 80),
+                ],
               ),
             ),
-            const SizedBox(height: 16),
-            // 明细列表卡片 — 表头 + 行
-            Padding(
-              padding: const EdgeInsets.symmetric(horizontal: 12),
-              child: Container(
-                padding: const EdgeInsets.all(16),
-                decoration: BoxDecoration(color: colors.bgCard, borderRadius: BorderRadius.circular(12)),
-                child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
-                  Row(children: [Bone.square(size: 20, borderRadius: BorderRadius.circular(4)), const SizedBox(width: 8), Bone(width: 80, height: 16)]),
+          ),
+        ),
+      ],
+    );
+  }
+}
+
+/// 费用报销明细报表骨架(ExpenseDetailReportPage)
+/// 布局:筛选栏 + 统计卡片(2+1) + 双折线图 + 柱形图 + 明细列表
+class SkeletonExpenseDetailReportPage extends StatelessWidget {
+  const SkeletonExpenseDetailReportPage({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    return Column(
+      children: [
+        _dateFilterChip(colors),
+        Expanded(
+          child: SingleChildScrollView(
+            child: Skeletonizer(
+              enabled: true,
+              child: Column(
+                children: [
+                  // 统计卡片 — 1行2列 + 1个全宽
+                  Padding(
+                    padding: const EdgeInsets.fromLTRB(12, 12, 12, 0),
+                    child: Row(
+                      children: [
+                        Expanded(
+                          child: Bone(
+                            height: 80,
+                            borderRadius: BorderRadius.circular(8),
+                          ),
+                        ),
+                        const SizedBox(width: 8),
+                        Expanded(
+                          child: Bone(
+                            height: 80,
+                            borderRadius: BorderRadius.circular(8),
+                          ),
+                        ),
+                      ],
+                    ),
+                  ),
+                  const SizedBox(height: 8),
+                  Padding(
+                    padding: const EdgeInsets.symmetric(horizontal: 12),
+                    child: Row(
+                      children: [
+                        Expanded(
+                          child: Bone(
+                            height: 80,
+                            borderRadius: BorderRadius.circular(8),
+                          ),
+                        ),
+                      ],
+                    ),
+                  ),
                   const SizedBox(height: 16),
-                  ...List.generate(6, (_) => Padding(
-                    padding: const EdgeInsets.only(bottom: 8),
-                    child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
-                      Bone(width: 60, height: 14),
-                      Bone(width: 100, height: 14),
-                      Bone(width: 80, height: 14),
-                      Bone(width: 60, height: 14),
-                    ]),
-                  )),
-                ]),
+                  // 双折线图 — 标题 + 2个图例 + 图表区
+                  Padding(
+                    padding: const EdgeInsets.symmetric(horizontal: 12),
+                    child: Container(
+                      padding: const EdgeInsets.all(16),
+                      decoration: BoxDecoration(
+                        color: colors.bgCard,
+                        borderRadius: BorderRadius.circular(8),
+                      ),
+                      child: Column(
+                        crossAxisAlignment: CrossAxisAlignment.start,
+                        children: [
+                          Bone(width: 120, height: 16),
+                          const SizedBox(height: 8),
+                          Row(
+                            children: [
+                              Bone.square(
+                                size: 8,
+                                borderRadius: BorderRadius.circular(4),
+                              ),
+                              const SizedBox(width: 4),
+                              Bone(width: 60, height: 12),
+                              const SizedBox(width: 16),
+                              Bone.square(
+                                size: 8,
+                                borderRadius: BorderRadius.circular(4),
+                              ),
+                              const SizedBox(width: 4),
+                              Bone(width: 60, height: 12),
+                            ],
+                          ),
+                          const SizedBox(height: 16),
+                          Row(
+                            children: [
+                              Expanded(
+                                child: Bone(
+                                  height: 200,
+                                  borderRadius: BorderRadius.circular(8),
+                                ),
+                              ),
+                            ],
+                          ),
+                        ],
+                      ),
+                    ),
+                  ),
+                  const SizedBox(height: 16),
+                  // 柱形图 — 标题 + 图表区
+                  Padding(
+                    padding: const EdgeInsets.symmetric(horizontal: 12),
+                    child: Container(
+                      padding: const EdgeInsets.all(16),
+                      decoration: BoxDecoration(
+                        color: colors.bgCard,
+                        borderRadius: BorderRadius.circular(8),
+                      ),
+                      child: Column(
+                        crossAxisAlignment: CrossAxisAlignment.start,
+                        children: [
+                          Bone(width: 160, height: 16),
+                          const SizedBox(height: 16),
+                          Row(
+                            children: [
+                              Expanded(
+                                child: Bone(
+                                  height: 220,
+                                  borderRadius: BorderRadius.circular(8),
+                                ),
+                              ),
+                            ],
+                          ),
+                        ],
+                      ),
+                    ),
+                  ),
+                  const SizedBox(height: 12),
+                  // 明细列表
+                  _detailListSkeleton(colors),
+                  const SizedBox(height: 80),
+                ],
               ),
             ),
-            const SizedBox(height: 24),
-          ],
+          ),
         ),
-      ),
+      ],
     );
   }
 }
+
+// ── 日期筛选 chip 占位 ──
+Widget _dateFilterChip(AppColorsExtension colors) => Container(
+  padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
+  color: colors.bgCard,
+  child: Container(
+    height: 40,
+    decoration: BoxDecoration(
+      color: colors.bgSecondaryContainer,
+      borderRadius: BorderRadius.circular(20),
+    ),
+  ),
+);
+
+// ── 明细列表骨架 ──
+Widget _detailListSkeleton(AppColorsExtension colors) => Padding(
+  padding: const EdgeInsets.symmetric(horizontal: 12),
+  child: Container(
+    width: double.infinity,
+    decoration: BoxDecoration(
+      color: colors.bgCard,
+      borderRadius: BorderRadius.circular(8),
+    ),
+    child: Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Padding(
+          padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
+          child: Row(
+            children: [
+              Bone(width: 80, height: 16),
+              const Spacer(),
+              Bone(width: 100, height: 14),
+            ],
+          ),
+        ),
+        const Divider(height: 1),
+        ...List.generate(4, (i) {
+          return Column(
+            children: [
+              Padding(
+                padding: const EdgeInsets.symmetric(
+                  horizontal: 16,
+                  vertical: 12,
+                ),
+                child: Column(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    Row(
+                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                      children: [
+                        Bone(width: 120 + (i % 3) * 40, height: 14),
+                        Bone(width: 80, height: 16),
+                      ],
+                    ),
+                    const SizedBox(height: 6),
+                    Row(
+                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                      children: [
+                        Bone(width: 90, height: 12),
+                        Bone(width: 80, height: 16),
+                      ],
+                    ),
+                    if (i == 0) ...[
+                      const SizedBox(height: 6),
+                      Bone(width: 200, height: 14),
+                    ],
+                  ],
+                ),
+              ),
+              if (i < 3) const Divider(height: 1),
+            ],
+          );
+        }),
+      ],
+    ),
+  ),
+);

+ 562 - 0
lib/shared/widgets/date_range_picker.dart

@@ -0,0 +1,562 @@
+import 'package:flutter/material.dart';
+import 'package:tdesign_flutter/tdesign_flutter.dart';
+import '../../core/i18n/app_localizations.dart';
+import '../../core/theme/app_colors_extension.dart';
+
+/// 通用日期区间选择器。
+///
+/// 关闭态为单个胶囊 chip(日历图标 + 日期区间文本),
+/// 点击弹出 TDSlidePopupRoute,内含快捷日期 chip、
+/// 起始/截止 TDDatePicker、已选区间展示、取消/确定按钮。
+class DateRangePicker extends StatefulWidget {
+  final TextEditingController startCtrl;
+  final TextEditingController endCtrl;
+  final VoidCallback? onChanged;
+
+  const DateRangePicker({
+    super.key,
+    required this.startCtrl,
+    required this.endCtrl,
+    this.onChanged,
+  });
+
+  @override
+  State<DateRangePicker> createState() => _DateRangePickerState();
+}
+
+class _DateRangePickerState extends State<DateRangePicker> {
+  late DateTime _tempStart;
+  late DateTime _tempEnd;
+
+  @override
+  void initState() {
+    super.initState();
+    _initTemp();
+  }
+
+  void _initTemp() {
+    final now = DateTime.now();
+    _tempStart =
+        DateTime.tryParse(widget.startCtrl.text) ??
+        DateTime(now.year, now.month, 1);
+    _tempEnd =
+        DateTime.tryParse(widget.endCtrl.text) ??
+        DateTime(now.year, now.month + 1, 0);
+  }
+
+  String get _displayText =>
+      '${widget.startCtrl.text} ~ ${widget.endCtrl.text}';
+
+  DateTime _today() => DateTime.now();
+
+  DateTime _dateOnly(DateTime d) => DateTime(d.year, d.month, d.day);
+
+  // ---- 弹窗 ----
+
+  void _openPicker() {
+    _initTemp();
+
+    final l10n = AppLocalizations.of(context);
+    final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final now = DateTime.now();
+
+    Navigator.of(context).push(
+      TDSlidePopupRoute(
+        builder: (ctx) => _PickerContent(
+          l10n: l10n,
+          colors: colors,
+          now: now,
+          tempStart: _tempStart,
+          tempEnd: _tempEnd,
+          onStartChanged: (d) => _tempStart = d,
+          onEndChanged: (d) => _tempEnd = d,
+          onQuick: (key) {
+            switch (key) {
+              case 'today':
+                final t = _dateOnly(_today());
+                return (start: t, end: t);
+              case 'thisWeek':
+                final t = _dateOnly(_today());
+                final wd = t.weekday;
+                final s = t.subtract(Duration(days: wd - 1));
+                return (start: s, end: s.add(const Duration(days: 6)));
+              case 'last7Days':
+                final t = _dateOnly(_today());
+                return (start: t.subtract(const Duration(days: 6)), end: t);
+              case 'thisMonth':
+                final t = _dateOnly(_today());
+                return (
+                  start: DateTime(t.year, t.month, 1),
+                  end: DateTime(t.year, t.month + 1, 0),
+                );
+              case 'lastMonth':
+                final t = _dateOnly(_today());
+                return (
+                  start: DateTime(t.year, t.month - 1, 1),
+                  end: DateTime(t.year, t.month, 0),
+                );
+              case 'last30Days':
+                final t = _dateOnly(_today());
+                return (start: t.subtract(const Duration(days: 29)), end: t);
+              case 'thisQuarter':
+                final t = _dateOnly(_today());
+                final q = (t.month - 1) ~/ 3;
+                return (
+                  start: DateTime(t.year, q * 3 + 1, 1),
+                  end: DateTime(t.year, q * 3 + 4, 0),
+                );
+              case 'thisYear':
+                final t = _dateOnly(_today());
+                return (
+                  start: DateTime(t.year, 1, 1),
+                  end: DateTime(t.year, 12, 31),
+                );
+              default:
+                return (start: _tempStart, end: _tempEnd);
+            }
+          },
+          onConfirm: () {
+            widget.startCtrl.text = _fmt(_tempStart);
+            widget.endCtrl.text = _fmt(_tempEnd);
+            widget.onChanged?.call();
+            Navigator.of(ctx).pop();
+          },
+          onCancel: () => Navigator.of(ctx).pop(),
+        ),
+      ),
+    );
+  }
+
+  String _fmt(DateTime d) =>
+      '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
+
+  // ---- 关闭态 chip ----
+
+  @override
+  Widget build(BuildContext context) {
+    final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final tdTheme = TDTheme.of(context);
+    final text = _displayText;
+
+    return GestureDetector(
+      onTap: _openPicker,
+      child: 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: 12),
+            Expanded(
+              child: Text(
+                text,
+                textAlign: TextAlign.center,
+                maxLines: 1,
+                overflow: TextOverflow.ellipsis,
+                style: TextStyle(fontSize: 14, color: colors.textPrimary),
+              ),
+            ),
+            const SizedBox(width: 4),
+            Icon(Icons.chevron_right, size: 16, color: colors.textSecondary),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+/// 弹窗内容(StatefulWidget,自行管理临时状态)
+class _PickerContent extends StatefulWidget {
+  final AppLocalizations l10n;
+  final AppColorsExtension colors;
+  final DateTime now;
+  final DateTime tempStart;
+  final DateTime tempEnd;
+  final void Function(DateTime d) onStartChanged;
+  final void Function(DateTime d) onEndChanged;
+  final ({DateTime start, DateTime end}) Function(String key) onQuick;
+  final VoidCallback onConfirm;
+  final VoidCallback onCancel;
+
+  const _PickerContent({
+    required this.l10n,
+    required this.colors,
+    required this.now,
+    required this.tempStart,
+    required this.tempEnd,
+    required this.onStartChanged,
+    required this.onEndChanged,
+    required this.onQuick,
+    required this.onConfirm,
+    required this.onCancel,
+  });
+
+  @override
+  State<_PickerContent> createState() => _PickerContentState();
+}
+
+class _PickerContentState extends State<_PickerContent> {
+  late DateTime _start;
+  late DateTime _end;
+
+  @override
+  void initState() {
+    super.initState();
+    _start = widget.tempStart;
+    _end = widget.tempEnd;
+  }
+
+  String _fmt(DateTime d) =>
+      '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
+
+  List<String> get _quickKeys => [
+    'today',
+    'thisWeek',
+    'last7Days',
+    'thisMonth',
+    'lastMonth',
+    'last30Days',
+    'thisQuarter',
+    'thisYear',
+  ];
+
+  String _quickLabel(String key) {
+    switch (key) {
+      case 'today':
+        return widget.l10n.get('today');
+      case 'thisWeek':
+        return widget.l10n.get('thisWeek');
+      case 'last7Days':
+        return widget.l10n.get('last7Days');
+      case 'thisMonth':
+        return widget.l10n.get('thisMonth');
+      case 'lastMonth':
+        return widget.l10n.get('lastMonth');
+      case 'last30Days':
+        return widget.l10n.get('last30Days');
+      case 'thisQuarter':
+        return widget.l10n.get('thisQuarter');
+      case 'thisYear':
+        return widget.l10n.get('thisYear');
+      default:
+        return key;
+    }
+  }
+
+  void _onQuickTap(String key) {
+    final r = widget.onQuick(key);
+    setState(() {
+      _start = r.start;
+      _end = r.end;
+    });
+  }
+
+  List<int> _toList(DateTime d) => [d.year, d.month, d.day];
+
+  /// 兼容 TDDatePicker.onChange 的 List / Map 两种回调格式
+  List<int> _parseSelected(dynamic selected) {
+    if (selected is List) {
+      return [
+        int.parse(selected[0].toString()),
+        int.parse(selected[1].toString()),
+        int.parse(selected[2].toString()),
+      ];
+    }
+    return [
+      int.parse(selected['year'].toString()),
+      int.parse(selected['month'].toString()),
+      int.parse(selected['day'].toString()),
+    ];
+  }
+
+  String? get _errorMessage {
+    if (_start.isAfter(_end)) {
+      return widget.l10n.get('filterDateStartAfterEnd');
+    }
+    if (_end.difference(_start).inDays > 365) {
+      return widget.l10n.get('dateRangeLimit');
+    }
+    return null;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final l10n = widget.l10n;
+    final colors = widget.colors;
+    final now = widget.now;
+
+    return ConstrainedBox(
+      constraints: BoxConstraints(
+        maxHeight: MediaQuery.of(context).size.height * 0.9,
+      ),
+      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, l10n),
+            // 内容区域(可滚动,适应不同屏幕高度)
+            Flexible(
+              child: SingleChildScrollView(
+                physics: const ClampingScrollPhysics(),
+                padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
+                child: Column(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    // 快捷日期
+                    Wrap(
+                      spacing: 8,
+                      runSpacing: 8,
+                      children: _quickKeys.map((key) {
+                        return GestureDetector(
+                          onTap: () => _onQuickTap(key),
+                          child: Container(
+                            padding: const EdgeInsets.symmetric(
+                              horizontal: 10,
+                              vertical: 4,
+                            ),
+                            decoration: BoxDecoration(
+                              color: colors.primary.withValues(alpha: 0.1),
+                              borderRadius: BorderRadius.circular(6),
+                              border: Border.all(
+                                color: colors.primary.withValues(alpha: 0.3),
+                                width: 0.5,
+                              ),
+                            ),
+                            child: Text(
+                              _quickLabel(key),
+                              style: TextStyle(
+                                fontSize: 13,
+                                color: colors.primary,
+                              ),
+                            ),
+                          ),
+                        );
+                      }).toList(),
+                    ),
+                    const SizedBox(height: 24),
+                    // 起始日期
+                    Text(
+                      l10n.get('startDateLabel'),
+                      style: TextStyle(
+                        fontSize: 14,
+                        fontWeight: FontWeight.w500,
+                        color: colors.textPrimary,
+                      ),
+                    ),
+                    const SizedBox(height: 8),
+                    ClipRRect(
+                      borderRadius: BorderRadius.circular(12),
+                      child: Container(
+                        decoration: BoxDecoration(color: colors.bgCard),
+                        child: TDDatePicker(
+                          key: ValueKey('start_${_fmt(_start)}'),
+                          header: false,
+                          model: DatePickerModel(
+                            useYear: true,
+                            useMonth: true,
+                            useDay: true,
+                            useHour: false,
+                            useMinute: false,
+                            useSecond: false,
+                            useWeekDay: false,
+                            dateStart: [2000, 1, 1],
+                            dateEnd: [now.year + 1, 12, 31],
+                            dateInitial: _toList(_start),
+                          ),
+                          onChange: (selected) {
+                            final vals = _parseSelected(selected);
+                            setState(() {
+                              _start = DateTime(vals[0], vals[1], vals[2]);
+                            });
+                          },
+                        ),
+                      ),
+                    ),
+                    const SizedBox(height: 24),
+                    // 截止日期
+                    Text(
+                      l10n.get('endDateLabel'),
+                      style: TextStyle(
+                        fontSize: 14,
+                        fontWeight: FontWeight.w500,
+                        color: colors.textPrimary,
+                      ),
+                    ),
+                    const SizedBox(height: 8),
+                    ClipRRect(
+                      borderRadius: BorderRadius.circular(12),
+                      child: Container(
+                        decoration: BoxDecoration(color: colors.bgCard),
+                        child: TDDatePicker(
+                          key: ValueKey('end_${_fmt(_end)}'),
+                          header: false,
+                          model: DatePickerModel(
+                            useYear: true,
+                            useMonth: true,
+                            useDay: true,
+                            useHour: false,
+                            useMinute: false,
+                            useSecond: false,
+                            useWeekDay: false,
+                            dateStart: [2000, 1, 1],
+                            dateEnd: [now.year + 1, 12, 31],
+                            dateInitial: _toList(_end),
+                          ),
+                          onChange: (selected) {
+                            final vals = _parseSelected(selected);
+                            setState(() {
+                              _end = DateTime(vals[0], vals[1], vals[2]);
+                            });
+                          },
+                        ),
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+            ),
+            // 校验提示 + 底部按钮
+            Container(
+              decoration: BoxDecoration(
+                color: colors.bgCard,
+                border: Border(
+                  top: BorderSide(color: colors.border, width: 0.5),
+                ),
+              ),
+              child: Column(
+                mainAxisSize: MainAxisSize.min,
+                children: [
+                  AnimatedSize(
+                    duration: const Duration(milliseconds: 200),
+                    curve: Curves.easeInOut,
+                    child: _errorMessage != null
+                        ? Padding(
+                            padding: const EdgeInsets.fromLTRB(16, 10, 16, 0),
+                            child: Row(
+                              children: [
+                                Icon(
+                                  Icons.error_outline,
+                                  size: 14,
+                                  color: colors.warning,
+                                ),
+                                const SizedBox(width: 6),
+                                Expanded(
+                                  child: Text(
+                                    _errorMessage!,
+                                    style: TextStyle(
+                                      fontSize: 12,
+                                      color: colors.warning,
+                                    ),
+                                  ),
+                                ),
+                              ],
+                            ),
+                          )
+                        : const SizedBox.shrink(),
+                  ),
+                  Padding(
+                    padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
+                    child: Row(
+                      children: [
+                        Expanded(
+                          child: TDButton(
+                            text: l10n.get('cancel'),
+                            size: TDButtonSize.large,
+                            type: TDButtonType.outline,
+                            shape: TDButtonShape.rectangle,
+                            theme: TDButtonTheme.defaultTheme,
+                            onTap: widget.onCancel,
+                          ),
+                        ),
+                        const SizedBox(width: 12),
+                        Expanded(
+                          child: TDButton(
+                            text: l10n.get('confirm'),
+                            size: TDButtonSize.large,
+                            type: TDButtonType.fill,
+                            shape: TDButtonShape.rectangle,
+                            theme: _errorMessage != null
+                                ? TDButtonTheme.defaultTheme
+                                : TDButtonTheme.primary,
+                            onTap: _errorMessage != null
+                                ? null
+                                : () {
+                                    widget.onStartChanged(_start);
+                                    widget.onEndChanged(_end);
+                                    widget.onConfirm();
+                                  },
+                          ),
+                        ),
+                      ],
+                    ),
+                  ),
+                ],
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildHeader(AppColorsExtension colors, AppLocalizations l10n) {
+    return Column(
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        Center(
+          child: Container(
+            margin: const EdgeInsets.only(top: 8, bottom: 4),
+            width: 36,
+            height: 4,
+            decoration: BoxDecoration(
+              color: colors.border,
+              borderRadius: BorderRadius.circular(2),
+            ),
+          ),
+        ),
+        Padding(
+          padding: const EdgeInsets.fromLTRB(20, 8, 12, 16),
+          child: Row(
+            children: [
+              const SizedBox(width: 24),
+              Expanded(
+                child: Center(
+                  child: Text(
+                    l10n.get('datePickerTitle'),
+                    style: TextStyle(
+                      fontSize: 18,
+                      fontWeight: FontWeight.w600,
+                      color: colors.textPrimary,
+                    ),
+                  ),
+                ),
+              ),
+              GestureDetector(
+                onTap: widget.onCancel,
+                child: Padding(
+                  padding: const EdgeInsets.all(4),
+                  child: Icon(
+                    Icons.close,
+                    size: 20,
+                    color: colors.textSecondary,
+                  ),
+                ),
+              ),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+}