chengc 2 settimane fa
parent
commit
e895509a9e

+ 3 - 0
lib/core/navigation/navigation_channel.dart

@@ -3,6 +3,9 @@ import 'package:go_router/go_router.dart';
 
 /// 平台(iOS/Android)主动下发路由时,通过此 Channel 传递给 Flutter。
 ///
+/// navigateTo:用 [GoRouter.go] 清空整个导航栈后跳转到目标路由。
+/// 配合 Android 端追加的 _ts 时间戳,确保缓存引擎下每次打开 Flutter 页面都重建 State。
+///
 /// iOS 侧调用方式:
 /// ```objc
 /// [engine.navigationChannel invokeMethod:@"pushRouteInformation"

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

@@ -39,25 +39,25 @@ GoRouter createAppRouter() {
       GoRoute(path: '/', pageBuilder: (_, _) => const MaterialPage(child: AppScaffold(showTabBar: true, body: HomePage()))),
       GoRoute(path: '/messages', pageBuilder: (_, _) => const MaterialPage(child: AppScaffold(showTabBar: true, body: MessageListPage()))),
       GoRoute(path: '/profile', pageBuilder: (_, _) => const MaterialPage(child: AppScaffold(showTabBar: true, body: ProfilePage()))),
-      GoRoute(path: '/expense/list', pageBuilder: (_, _) => const MaterialPage(child: AppScaffold(body: ExpenseListPage()))),
+      GoRoute(path: '/expense/list', pageBuilder: (_, state) => MaterialPage(key: ValueKey(state.uri.toString()), child: AppScaffold(body: ExpenseListPage()))),
       GoRoute(path: '/expense/import-apply', pageBuilder: (_, _) => const MaterialPage(child: AppScaffold(body: ExpenseApplyImportPage(), resizeToAvoidBottomInset: false))),
-      GoRoute(path: '/expense/create', pageBuilder: (_, state) => MaterialPage(child: AppScaffold(body: ExpenseCreatePage(editId: state.uri.queryParameters['id']), resizeToAvoidBottomInset: false))),
-      GoRoute(path: '/expense/detail/:billNo', pageBuilder: (_, state) => MaterialPage(child: AppScaffold(body: ExpenseDetailPage(billNo: state.pathParameters['billNo']!, queryId: int.tryParse(state.uri.queryParameters['queryId'] ?? '0') ?? 0)))),
-      GoRoute(path: '/overtime/list', pageBuilder: (_, _) => const MaterialPage(child: AppScaffold(body: OvertimeListPage()))),
+      GoRoute(path: '/expense/create', pageBuilder: (_, state) => MaterialPage(key: ValueKey(state.uri.toString()), child: AppScaffold(body: ExpenseCreatePage(editId: state.uri.queryParameters['id']), resizeToAvoidBottomInset: false))),
+      GoRoute(path: '/expense/detail/:billNo', pageBuilder: (_, state) => MaterialPage(key: ValueKey(state.uri.toString()), child: AppScaffold(body: ExpenseDetailPage(billNo: state.pathParameters['billNo']!, queryId: int.tryParse(state.uri.queryParameters['queryId'] ?? '0') ?? 0)))),
+      GoRoute(path: '/overtime/list', pageBuilder: (_, state) => MaterialPage(key: ValueKey(state.uri.toString()), child: AppScaffold(body: OvertimeListPage()))),
       GoRoute(path: '/overtime/create', pageBuilder: (_, state) => MaterialPage(child: AppScaffold(body: OvertimeCreatePage(editId: state.uri.queryParameters['id'])))),
       GoRoute(path: '/overtime/detail/:id', pageBuilder: (_, state) => MaterialPage(child: AppScaffold(body: OvertimeDetailPage(id: state.pathParameters['id']!)))),
-      GoRoute(path: '/vehicle/list', pageBuilder: (_, _) => const MaterialPage(child: AppScaffold(body: VehicleListPage()))),
+      GoRoute(path: '/vehicle/list', pageBuilder: (_, state) => MaterialPage(key: ValueKey(state.uri.toString()), child: AppScaffold(body: VehicleListPage()))),
       GoRoute(path: '/vehicle/create', pageBuilder: (_, state) => MaterialPage(child: AppScaffold(body: VehicleCreatePage(editId: state.uri.queryParameters['id'])))),
       GoRoute(path: '/vehicle/detail/:id', pageBuilder: (_, state) => MaterialPage(child: AppScaffold(body: VehicleDetailPage(id: state.pathParameters['id']!)))),
-      GoRoute(path: '/outing-log/list', pageBuilder: (_, _) => const MaterialPage(child: AppScaffold(body: OutingLogListPage()))),
+      GoRoute(path: '/outing-log/list', pageBuilder: (_, state) => MaterialPage(key: ValueKey(state.uri.toString()), child: AppScaffold(body: OutingLogListPage()))),
       GoRoute(path: '/outing-log/create', pageBuilder: (_, _) => const MaterialPage(child: AppScaffold(body: OutingLogCreatePage()))),
       GoRoute(path: '/outing-log/detail/:id', pageBuilder: (_, state) => MaterialPage(child: AppScaffold(body: OutingLogDetailPage(id: state.pathParameters['id']!)))),
-      GoRoute(path: '/announcement/list', pageBuilder: (_, _) => const MaterialPage(child: AppScaffold(body: AnnouncementListPage()))),
+      GoRoute(path: '/announcement/list', pageBuilder: (_, state) => MaterialPage(key: ValueKey(state.uri.toString()), child: AppScaffold(body: AnnouncementListPage()))),
       GoRoute(path: '/announcement/detail/:id', pageBuilder: (_, state) => MaterialPage(child: AppScaffold(body: AnnouncementDetailPage(id: state.pathParameters['id']!)))),
       GoRoute(path: '/announcement/create', pageBuilder: (_, _) => const MaterialPage(child: AppScaffold(body: AnnouncementCreatePage()))),
-      GoRoute(path: '/expense-apply/list', pageBuilder: (_, _) => const MaterialPage(child: AppScaffold(body: ExpenseApplyListPage()))),
-      GoRoute(path: '/expense-apply/detail/:billNo', pageBuilder: (_, state) => MaterialPage(child: AppScaffold(body: ExpenseApplyDetailPage(billNo: state.pathParameters['billNo']!, queryId: int.tryParse(state.uri.queryParameters['queryId'] ?? '0') ?? 0)))),
-      GoRoute(path: '/expense-apply/create', pageBuilder: (_, state) => MaterialPage(child: AppScaffold(body: ExpenseApplyCreatePage(id: state.uri.queryParameters['id']), resizeToAvoidBottomInset: false))),
+      GoRoute(path: '/expense-apply/list', pageBuilder: (_, state) => MaterialPage(key: ValueKey(state.uri.toString()), child: AppScaffold(body: ExpenseApplyListPage()))),
+      GoRoute(path: '/expense-apply/detail/:billNo', pageBuilder: (_, state) => MaterialPage(key: ValueKey(state.uri.toString()), child: AppScaffold(body: ExpenseApplyDetailPage(billNo: state.pathParameters['billNo']!, queryId: int.tryParse(state.uri.queryParameters['queryId'] ?? '0') ?? 0)))),
+      GoRoute(path: '/expense-apply/create', pageBuilder: (_, state) => MaterialPage(key: ValueKey(state.uri.toString()), child: AppScaffold(body: ExpenseApplyCreatePage(id: state.uri.queryParameters['id']), resizeToAvoidBottomInset: false))),
       GoRoute(path: '/report/expense-detail', pageBuilder: (_, _) => const MaterialPage(child: AppScaffold(body: ExpenseDetailReportPage()))),
       GoRoute(path: '/report/expense-apply-detail', pageBuilder: (_, _) => const MaterialPage(child: AppScaffold(body: ExpenseApplyDetailReportPage()))),
       GoRoute(path: '/report/overtime-detail', pageBuilder: (_, _) => const MaterialPage(child: AppScaffold(body: OvertimeDetailReportPage()))),

+ 30 - 20
lib/features/announcement/announcement_list_page.dart

@@ -8,7 +8,7 @@ import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../core/utils/responsive.dart';
 import '../../shared/widgets/empty_state.dart';
-import '../../shared/widgets/skeleton_list_card.dart';
+import '../../shared/widgets/app_skeletons.dart';
 import '../../shared/widgets/list_footer.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../../core/auth/role_provider.dart';
@@ -42,12 +42,20 @@ class _AnnouncementListPageState extends ConsumerState<AnnouncementListPage>
           l10n.get('holidayActivity'),
         ];
 
+  bool _firstBuild = true;
+  bool _invalidateDone = false;
+
   @override
   void initState() {
     super.initState();
     final isAdmin = ref.read(isAdminProvider);
     _tabCtrl = TabController(length: isAdmin ? 5 : 4, vsync: this);
     _tabCtrl.addListener(_onTabChanged);
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      ref.invalidate(filteredAnnouncementsProvider(0));
+      _invalidateDone = true;
+      setState(() {});
+    });
   }
 
   void _onTabChanged() {
@@ -64,6 +72,16 @@ class _AnnouncementListPageState extends ConsumerState<AnnouncementListPage>
 
   @override
   Widget build(BuildContext context) {
+    if (_invalidateDone) {
+      ref.listen(filteredAnnouncementsProvider(0), (prev, next) {
+        if (_firstBuild && !next.isLoading && !next.isReloading) {
+          setState(() => _firstBuild = false);
+          WidgetsBinding.instance.addPostFrameCallback((_) {
+            if (mounted) setState(() {});
+          });
+        }
+      });
+    }
     final isAdmin = ref.watch(isAdminProvider);
     final l10n = AppLocalizations.of(context);
     final tabs = _getTabLabels(isAdmin, l10n);
@@ -163,12 +181,14 @@ class _AnnouncementListPageState extends ConsumerState<AnnouncementListPage>
             Expanded(
               child: Container(
                 color: colors.bgPage,
-                child: TabBarView(
-                  controller: _tabCtrl,
-                  children: List.generate(tabs.length, (tabIdx) {
-                    return _AnnouncementTabContent(tabIndex: tabIdx);
-                  }),
-                ),
+                child: _firstBuild
+                    ? const Center(child: SkeletonLoadingList())
+                    : TabBarView(
+                        controller: _tabCtrl,
+                        children: List.generate(tabs.length, (tabIdx) {
+                          return _AnnouncementTabContent(tabIndex: tabIdx);
+                        }),
+                      ),
               ),
             ),
           ],
@@ -176,6 +196,7 @@ class _AnnouncementListPageState extends ConsumerState<AnnouncementListPage>
       ),
     );
   }
+
 }
 
 class _AnnouncementTabContent extends ConsumerWidget {
@@ -210,19 +231,8 @@ class _AnnouncementTabContent extends ConsumerWidget {
     final l10n = AppLocalizations.of(context);
     if (itemsAsync.isReloading) {
       final oldItems = itemsAsync.valueOrNull ?? [];
-      if (oldItems.isEmpty) {
-        return ListView(
-          children: [
-            const SizedBox(height: 120),
-            EmptyState(message: l10n.get('noAnnouncements')),
-          ],
-        );
-      }
-      return ListView.builder(
-        padding: const EdgeInsets.symmetric(vertical: 8),
-        itemCount: oldItems.length,
-        itemBuilder: (_, i) => _buildAnnouncementCard(context, oldItems[i]),
-      );
+      if (oldItems.isEmpty) return ListView(children: [const SizedBox(height: 120), EmptyState(message: l10n.get('noAnnouncements'))]);
+      return ListView.builder(padding: const EdgeInsets.symmetric(vertical: 8), itemCount: oldItems.length, itemBuilder: (_, i) => _buildAnnouncementCard(context, oldItems[i]));
     }
 
     if (itemsAsync.hasError) {

+ 1 - 1
lib/features/expense/expense_apply_import_page.dart

@@ -7,7 +7,7 @@ import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../shared/widgets/empty_state.dart';
-import '../../shared/widgets/skeleton_list_card.dart';
+import '../../shared/widgets/app_skeletons.dart';
 import '../../shared/widgets/list_footer.dart';
 import 'expense_api.dart';
 

+ 19 - 46
lib/features/expense/expense_create_page.dart

@@ -10,6 +10,7 @@ import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/utils/responsive.dart';
 import '../../shared/widgets/form_section.dart';
 import '../../shared/widgets/form_field_row.dart';
+import '../../shared/widgets/app_skeletons.dart';
 import 'expense_api.dart';
 import 'expense_create_controller.dart';
 import '../../core/i18n/app_localizations.dart';
@@ -32,8 +33,7 @@ class ExpenseCreatePage extends ConsumerStatefulWidget {
   ConsumerState<ExpenseCreatePage> createState() => _ExpenseCreatePageState();
 }
 
-class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage>
-    with WidgetsBindingObserver {
+class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage> {
   final _purposeController = TextEditingController();
   final _purposeFocus = FocusNode();
   final _remarkController = TextEditingController();
@@ -44,8 +44,6 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage>
   bool _attachAvailable = false;
   late Future<bool> _draftFuture;
   bool _draftHandled = false;
-  bool _isPoppingToNative = false;
-
   // ── 参考数据(从 API 加载) ──
   List<CostTypeItem> _costTypes = [];
   List<ProjectCodeItem> _projects = [];
@@ -53,6 +51,7 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage>
   List<CustomerItem> _customers = [];
   List<CurrencyItem> _currencies = [];
   List<EmployeeItem> _employees = [];
+  bool _firstBuild = true;
   bool _refDataLoading = true;
   bool _addingDetail = false;
 
@@ -63,7 +62,6 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage>
   @override
   void initState() {
     super.initState();
-    WidgetsBinding.instance.addObserver(this);
     SystemChrome.setSystemUIOverlayStyle(
       const SystemUiOverlayStyle(
         statusBarColor: Colors.transparent,
@@ -86,6 +84,18 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage>
         ? ExpenseCreateController.hasDraft()
         : Future.value(false);
     _loadRefData();
+    WidgetsBinding.instance.addPostFrameCallback((_) => _checkDataReady());
+  }
+
+  void _checkDataReady() {
+    if (!_refDataLoading && mounted) {
+      setState(() => _firstBuild = false);
+      WidgetsBinding.instance.addPostFrameCallback((_) {
+        if (mounted) setState(() {});
+      });
+    } else if (mounted) {
+      WidgetsBinding.instance.addPostFrameCallback((_) => _checkDataReady());
+    }
   }
 
   Future<void>? _refDataFuture;
@@ -161,7 +171,6 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage>
 
   @override
   void dispose() {
-    WidgetsBinding.instance.removeObserver(this);
     _purposeController.dispose();
     _purposeFocus.dispose();
     _remarkController.dispose();
@@ -172,48 +181,15 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage>
   }
 
   @override
-  void didChangeAppLifecycleState(AppLifecycleState state) {
-    if (state == AppLifecycleState.resumed) {
-      FocusScope.of(context).unfocus();
-      if (_isPoppingToNative) {
-        _isPoppingToNative = false;
-        HostAppChannel.refresh();
-        // 重置表单数据
-        _purposeController.clear();
-        _remarkController.clear();
-        _selectedDeptId = '';
-        _selectedDeptName = '';
-        _attachmentController.clear();
-        _attachAvailable = false;
-        _addingDetail = false;
-        // 重置参考数据
-        _costTypes = [];
-        _projects = [];
-        _departments = [];
-        _customers = [];
-        _employees = [];
-        _currencies = [];
-        _refDataFuture = null;
-        _refDataLoading = true;
-        // 重置草稿和控制器状态
-        _draftHandled = false;
-        _draftFuture = widget.editId == null
-            ? ExpenseCreateController.hasDraft()
-            : Future.value(false);
-        ref.read(expenseCreateProvider(widget.editId).notifier).reset();
-        // 重新加载
-        _loadRefData();
-        _checkAttachHealth();
-      }
-    }
-  }
-
-  @override
   Widget build(BuildContext context) {
     final controller = ref.watch(expenseCreateProvider(widget.editId).notifier);
     final state = ref.watch(expenseCreateProvider(widget.editId));
     final r = ResponsiveHelper.of(context);
     final l10n = AppLocalizations.of(context);
+    final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final bottomInset = MediaQuery.of(context).padding.bottom;
+
+    if (_firstBuild) return const Scaffold(body: SkeletonFormPage(showImportLink: true));
 
     setNavBarTitle(
       context,
@@ -226,8 +202,6 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage>
         onBack: () => _doPop(),
       ),
     );
-    final colors = Theme.of(context).extension<AppColorsExtension>()!;
-    final bottomInset = MediaQuery.of(context).padding.bottom;
 
     Widget pageContent = PopScope(
       canPop: false,
@@ -1358,7 +1332,6 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage>
     if (router.canPop()) {
       router.pop();
     } else {
-      _isPoppingToNative = true;
       SystemNavigator.pop();
     }
   }

+ 4 - 26
lib/features/expense/expense_detail_page.dart

@@ -8,6 +8,7 @@ import '../../shared/widgets/loading_dialog.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../shared/widgets/form_section.dart';
 import '../../shared/widgets/form_field_row.dart';
+import '../../shared/widgets/app_skeletons.dart';
 import 'expense_model.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../../shared/models/bill_attachment.dart';
@@ -29,7 +30,7 @@ class ExpenseDetailPage extends ConsumerStatefulWidget {
 }
 
 class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
-    with WidgetsBindingObserver {
+{
   ExpenseModel? _expense;
   List<BillAttachment> _attachments = [];
   bool _attachAvailable = false;
@@ -39,31 +40,14 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
   @override
   void initState() {
     super.initState();
-    WidgetsBinding.instance.addObserver(this);
     _loadData();
   }
 
   @override
   void dispose() {
-    WidgetsBinding.instance.removeObserver(this);
     super.dispose();
   }
 
-  bool _openingFile = false;
-
-  @override
-  void didChangeAppLifecycleState(AppLifecycleState state) {
-    if (state == AppLifecycleState.resumed) {
-      if (_openingFile) {
-        _openingFile = false;
-        return;
-      }
-      _attachments = [];
-      _attachAvailable = false;
-      _loadData();
-    }
-  }
-
   Future<void> _loadData() async {
     setState(() {
       _isLoading = true;
@@ -91,7 +75,7 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
     } catch (e) {
       setState(() => _error = e.toString());
     } finally {
-      setState(() => _isLoading = false);
+      setState(() { _isLoading = false; });
     }
   }
 
@@ -107,12 +91,7 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
     ));
 
     if (_isLoading) {
-      return const Center(
-        child: TDLoading(
-          size: TDLoadingSize.large,
-          icon: TDLoadingIcon.activity,
-        ),
-      );
+      return const SkeletonDetailPage();
     }
 
     if (_error != null) {
@@ -477,7 +456,6 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
       final dir = await getTemporaryDirectory();
       final file = File('${dir.path}/${a.fileName}');
       await file.writeAsBytes(bytes);
-      _openingFile = true;
       await OpenFilex.open(file.path);
     } catch (_) {
       if (mounted) LoadingDialog.hide(context);

+ 27 - 24
lib/features/expense/expense_list_page.dart

@@ -10,7 +10,7 @@ import '../../core/utils/date_utils.dart' as du;
 import '../../core/utils/responsive.dart';
 import '../../shared/widgets/list_card.dart';
 import '../../shared/widgets/empty_state.dart';
-import '../../shared/widgets/skeleton_list_card.dart';
+import '../../shared/widgets/app_skeletons.dart';
 import '../../shared/widgets/list_footer.dart';
 import '../../core/i18n/app_localizations.dart';
 import 'expense_list_controller.dart';
@@ -24,12 +24,13 @@ class ExpenseListPage extends ConsumerStatefulWidget {
   ConsumerState<ExpenseListPage> createState() => _ExpenseListPageState();
 }
 
-class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
-    with WidgetsBindingObserver {
+class _ExpenseListPageState extends ConsumerState<ExpenseListPage> {
   final _keywordCtrl = TextEditingController();
   final _startDateCtrl = TextEditingController();
   final _endDateCtrl = TextEditingController();
   late final EasyRefreshController _refreshCtrl;
+  bool _firstBuild = true;
+  bool _invalidateDone = false;
 
   @override
   void initState() {
@@ -41,28 +42,14 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
     WidgetsBinding.instance.addPostFrameCallback((_) {
       ref.read(expenseDateStartProvider.notifier).state = DateTime(now.year, now.month, 1);
       ref.read(expenseDateEndProvider.notifier).state = DateTime(now.year, now.month, _daysInMonth(now.year, now.month));
+      ref.invalidate(expenseMyListProvider(''));
+      _invalidateDone = true;
+      setState(() {});
     });
-    WidgetsBinding.instance.addObserver(this);
-  }
-
-  @override
-  void didChangeAppLifecycleState(AppLifecycleState state) {
-    if (state == AppLifecycleState.resumed) {
-      FocusScope.of(context).unfocus();
-      _keywordCtrl.clear();
-      final now = DateTime.now();
-      _startDateCtrl.text = '${now.year}-${now.month.toString().padLeft(2, '0')}-01';
-      _endDateCtrl.text = '${now.year}-${now.month.toString().padLeft(2, '0')}-${_daysInMonth(now.year, now.month).toString().padLeft(2, '0')}';
-      ref.read(expenseKeywordProvider.notifier).state = '';
-      ref.read(expenseDateStartProvider.notifier).state = DateTime(now.year, now.month, 1);
-      ref.read(expenseDateEndProvider.notifier).state = DateTime(now.year, now.month, _daysInMonth(now.year, now.month));
-      ref.read(expenseRefreshProvider.notifier).state++;
-    }
   }
 
   @override
   void dispose() {
-    WidgetsBinding.instance.removeObserver(this);
     _keywordCtrl.dispose();
     _startDateCtrl.dispose();
     _endDateCtrl.dispose();
@@ -92,10 +79,14 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
   int _daysInMonth(int year, int month) => DateTime(year, month + 1, 0).day;
 
   void _applyFilter() {
+    _refreshCtrl.callRefresh();
+  }
+
+  Future<void> _doRefresh() async {
     ref.read(expenseKeywordProvider.notifier).state = _keywordCtrl.text.trim();
     ref.read(expenseDateStartProvider.notifier).state = _startDateCtrl.text.isNotEmpty ? DateTime.tryParse(_startDateCtrl.text) : null;
     ref.read(expenseDateEndProvider.notifier).state = _endDateCtrl.text.isNotEmpty ? DateTime.tryParse(_endDateCtrl.text) : null;
-    _refreshCtrl.callRefresh();
+    ref.read(expenseRefreshProvider.notifier).state++;
   }
 
   Widget _dateChip(TextEditingController ctrl, String hint, TDThemeData tdTheme, AppColorsExtension colors) {
@@ -117,6 +108,17 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final tdTheme = TDTheme.of(context);
 
+    if (_invalidateDone) {
+      ref.listen(expenseMyListProvider(''), (prev, next) {
+        if (_firstBuild && !next.isLoading && !next.isReloading) {
+          setState(() => _firstBuild = false);
+          WidgetsBinding.instance.addPostFrameCallback((_) {
+            if (mounted) setState(() {});
+          });
+        }
+      });
+    }
+
     setNavBarTitle(context, ref, NavBarConfig(
       title: l10n.get('expenseList'),
       showBack: true,
@@ -169,7 +171,7 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
             ])),
           ]),
         ),
-        Expanded(child: Container(color: colors.bgPage, child: _ExpenseListContent(refreshCtrl: _refreshCtrl))),
+        Expanded(child: Container(color: colors.bgPage, child: _firstBuild ? const Center(child: SkeletonLoadingList()) : _ExpenseListContent(refreshCtrl: _refreshCtrl, onRefresh: _doRefresh))),
       ]),
       floatingActionButton: FloatingActionButton(
         onPressed: () => context.push('/report/expense-detail'),
@@ -183,14 +185,15 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
 
 class _ExpenseListContent extends ConsumerWidget {
   final EasyRefreshController refreshCtrl;
-  const _ExpenseListContent({required this.refreshCtrl});
+  final Future<void> Function() onRefresh;
+  const _ExpenseListContent({required this.refreshCtrl, required this.onRefresh});
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
     final r = ResponsiveHelper.of(context);
     final itemsAsync = ref.watch(expenseMyListProvider(''));
     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: () async { ref.read(expenseRefreshProvider.notifier).state++; }, child: _buildContent(itemsAsync, context, ref))));
+    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<ExpenseModel>> itemsAsync, BuildContext context, WidgetRef ref) {

+ 21 - 44
lib/features/expense_apply/expense_apply_create_page.dart

@@ -12,6 +12,7 @@ import '../../shared/widgets/action_bar.dart';
 import '../../shared/widgets/loading_dialog.dart';
 import '../../shared/widgets/form_section.dart';
 import '../../shared/widgets/form_field_row.dart';
+import '../../shared/widgets/app_skeletons.dart';
 import '../../shared/widgets/nav_bar_config.dart';
 import '../../shared/widgets/attachment_picker.dart';
 import '../../core/theme/app_colors.dart';
@@ -30,8 +31,7 @@ class ExpenseApplyCreatePage extends ConsumerStatefulWidget {
       _ExpenseApplyCreatePageState();
 }
 
-class _ExpenseApplyCreatePageState extends ConsumerState<ExpenseApplyCreatePage>
-    with WidgetsBindingObserver {
+class _ExpenseApplyCreatePageState extends ConsumerState<ExpenseApplyCreatePage> {
   static const _draftKey = 'expense_apply';
 
   // ── 基本信息 ──
@@ -55,12 +55,12 @@ class _ExpenseApplyCreatePageState extends ConsumerState<ExpenseApplyCreatePage>
   // ── 草稿 ──
   late Future<bool> _draftFuture;
   bool _draftHandled = false;
-  bool _isPoppingToNative = false;
 
   // ── 参考数据(从 API 加载) ──
   List<CostTypeItem> _costTypes = [];
   List<ProjectCodeItem> _projects = [];
   List<DepartmentItem> _departments = [];
+  bool _firstBuild = true;
   bool _refDataLoading = true;
   bool _addingDetail = false;
   dynamic _acctTree;
@@ -72,7 +72,6 @@ class _ExpenseApplyCreatePageState extends ConsumerState<ExpenseApplyCreatePage>
   @override
   void initState() {
     super.initState();
-    WidgetsBinding.instance.addObserver(this);
     SystemChrome.setSystemUIOverlayStyle(
       const SystemUiOverlayStyle(
         statusBarColor: Colors.transparent,
@@ -92,6 +91,19 @@ class _ExpenseApplyCreatePageState extends ConsumerState<ExpenseApplyCreatePage>
     _refDataFuture = null;
     _draftFuture = DraftStorage.has(_draftKey);
     _loadRefData();
+    WidgetsBinding.instance.addPostFrameCallback((_) => _checkDataReady());
+  }
+
+  void _checkDataReady() {
+    if (!_refDataLoading && mounted) {
+      setState(() => _firstBuild = false);
+      // 延迟一帧确保 ModalRoute.isCurrent 为 true,nav bar 配置正确生效
+      WidgetsBinding.instance.addPostFrameCallback((_) {
+        if (mounted) setState(() {});
+      });
+    } else if (mounted) {
+      WidgetsBinding.instance.addPostFrameCallback((_) => _checkDataReady());
+    }
   }
 
   Future<void>? _refDataFuture;
@@ -120,7 +132,6 @@ class _ExpenseApplyCreatePageState extends ConsumerState<ExpenseApplyCreatePage>
         _projects = results[1] as List<ProjectCodeItem>;
         _departments = results[2] as List<DepartmentItem>;
         _acctTree = _convertAcctTree(results[3]);
-        debugPrint('[_loadRefData] _acctTree isNull: ${_acctTree == null}');
         _refDataLoading = false;
         _autoSelectDept();
       });
@@ -167,7 +178,6 @@ class _ExpenseApplyCreatePageState extends ConsumerState<ExpenseApplyCreatePage>
 
   @override
   void dispose() {
-    WidgetsBinding.instance.removeObserver(this);
     _purposeController.dispose();
     _purposeFocus.dispose();
     _referenceNoController.dispose();
@@ -179,45 +189,13 @@ class _ExpenseApplyCreatePageState extends ConsumerState<ExpenseApplyCreatePage>
   }
 
   @override
-  void didChangeAppLifecycleState(AppLifecycleState state) {
-    if (state == AppLifecycleState.resumed) {
-      FocusScope.of(context).unfocus();
-      if (_isPoppingToNative) {
-        _isPoppingToNative = false;
-        HostAppChannel.refresh();
-        // 重置表单数据
-        _urgency = Urgency.normal.value;
-        _purposeController.clear();
-        _validUntil = '';
-        _referenceNoController.clear();
-        _remarkController.clear();
-        _details.clear();
-        _detailIdCounter = 1;
-        _attachmentController.clear();
-        _attachAvailable = false;
-        _addingDetail = false;
-        _selectedDeptId = '';
-        _selectedDeptName = '';
-        // 重置参考数据
-        _costTypes = [];
-        _projects = [];
-        _departments = [];
-        _acctTree = null;
-        _refDataFuture = null;
-        _refDataLoading = true;
-        // 重置草稿状态
-        _draftHandled = false;
-        _draftFuture = DraftStorage.has(_draftKey);
-        // 重新加载
-        _loadRefData();
-        _checkAttachHealth();
-      }
-    }
-  }
-
-  @override
   Widget build(BuildContext context) {
     final l10n = AppLocalizations.of(context);
+
+    if (_firstBuild) {
+      return const Scaffold(body: SkeletonFormPage());
+    }
+
     setNavBarTitle(
       context,
       ref,
@@ -1103,7 +1081,6 @@ class _ExpenseApplyCreatePageState extends ConsumerState<ExpenseApplyCreatePage>
     if (router.canPop()) {
       router.pop();
     } else {
-      _isPoppingToNative = true;
       SystemNavigator.pop();
     }
   }

+ 3 - 22
lib/features/expense_apply/expense_apply_detail_page.dart

@@ -12,6 +12,7 @@ import '../../shared/widgets/loading_dialog.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../shared/widgets/form_section.dart';
 import '../../shared/widgets/form_field_row.dart';
+import '../../shared/widgets/app_skeletons.dart';
 import 'expense_apply_model.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../../shared/models/bill_attachment.dart';
@@ -29,7 +30,7 @@ class ExpenseApplyDetailPage extends ConsumerStatefulWidget {
 }
 
 class _ExpenseApplyDetailPageState extends ConsumerState<ExpenseApplyDetailPage>
-    with WidgetsBindingObserver {
+{
   bool _loading = true;
   String? _error;
   ExpenseApplyModel? _data;
@@ -39,31 +40,14 @@ class _ExpenseApplyDetailPageState extends ConsumerState<ExpenseApplyDetailPage>
   @override
   void initState() {
     super.initState();
-    WidgetsBinding.instance.addObserver(this);
     _loadData();
   }
 
   @override
   void dispose() {
-    WidgetsBinding.instance.removeObserver(this);
     super.dispose();
   }
 
-  bool _openingFile = false;
-
-  @override
-  void didChangeAppLifecycleState(AppLifecycleState state) {
-    if (state == AppLifecycleState.resumed) {
-      if (_openingFile) {
-        _openingFile = false;
-        return;
-      }
-      _attachments = [];
-      _attachAvailable = false;
-      _loadData();
-    }
-  }
-
   Future<void> _loadData() async {
     setState(() { _loading = true; _error = null; });
     try {
@@ -119,9 +103,7 @@ class _ExpenseApplyDetailPageState extends ConsumerState<ExpenseApplyDetailPage>
     ));
 
     if (_loading) {
-      return const Center(
-        child: TDLoading(size: TDLoadingSize.large, icon: TDLoadingIcon.activity),
-      );
+      return const SkeletonDetailPage();
     }
 
     if (_error != null) {
@@ -403,7 +385,6 @@ class _ExpenseApplyDetailPageState extends ConsumerState<ExpenseApplyDetailPage>
       final dir = await getTemporaryDirectory();
       final file = File('${dir.path}/${a.fileName}');
       await file.writeAsBytes(bytes);
-      _openingFile = true;
       await OpenFilex.open(file.path);
     } catch (_) {
       if (mounted) LoadingDialog.hide(context);

+ 29 - 24
lib/features/expense_apply/expense_apply_list_page.dart

@@ -10,7 +10,7 @@ import '../../core/utils/date_utils.dart' as du;
 import '../../core/utils/responsive.dart';
 import '../../shared/widgets/list_card.dart';
 import '../../shared/widgets/empty_state.dart';
-import '../../shared/widgets/skeleton_list_card.dart';
+import '../../shared/widgets/app_skeletons.dart';
 import '../../shared/widgets/list_footer.dart';
 import '../../core/i18n/app_localizations.dart';
 import 'expense_apply_list_controller.dart';
@@ -22,12 +22,13 @@ class ExpenseApplyListPage extends ConsumerStatefulWidget {
   ConsumerState<ExpenseApplyListPage> createState() => _ExpenseApplyListPageState();
 }
 
-class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage>
-    with WidgetsBindingObserver {
+class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage> {
   final _keywordCtrl = TextEditingController();
   final _startDateCtrl = TextEditingController();
   final _endDateCtrl = TextEditingController();
   late final EasyRefreshController _refreshCtrl;
+  bool _firstBuild = true;
+  bool _invalidateDone = false;
 
   @override
   void initState() {
@@ -39,28 +40,14 @@ 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.invalidate(expenseApplyMyListProvider(''));
+      _invalidateDone = true;
+      setState(() {}); // 触发重建以挂载 ref.listen
     });
-    WidgetsBinding.instance.addObserver(this);
-  }
-
-  @override
-  void didChangeAppLifecycleState(AppLifecycleState state) {
-    if (state == AppLifecycleState.resumed) {
-      FocusScope.of(context).unfocus();
-      _keywordCtrl.clear();
-      final now = DateTime.now();
-      _startDateCtrl.text = '${now.year}-${now.month.toString().padLeft(2, '0')}-01';
-      _endDateCtrl.text = '${now.year}-${now.month.toString().padLeft(2, '0')}-${DateTime(now.year, now.month + 1, 0).day.toString().padLeft(2, '0')}';
-      ref.read(expenseApplyKeywordProvider.notifier).state = '';
-      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(expenseApplyRefreshProvider.notifier).state++;
-    }
   }
 
   @override
   void dispose() {
-    WidgetsBinding.instance.removeObserver(this);
     _keywordCtrl.dispose();
     _startDateCtrl.dispose();
     _endDateCtrl.dispose();
@@ -86,10 +73,14 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage>
   }
 
   void _applyFilter() {
+    _refreshCtrl.callRefresh();
+  }
+
+  Future<void> _doRefresh() async {
     ref.read(expenseApplyKeywordProvider.notifier).state = _keywordCtrl.text.trim();
     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;
-    _refreshCtrl.callRefresh();
+    ref.read(expenseApplyRefreshProvider.notifier).state++;
   }
 
   Widget _dateChip(TextEditingController ctrl, String hint, TDThemeData tdTheme, AppColorsExtension colors) {
@@ -111,6 +102,18 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage>
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final tdTheme = TDTheme.of(context);
 
+    // invalidate 后才挂载监听,避免首帧 ref.listen 触发不必要的 Provider 求值
+    if (_invalidateDone) {
+      ref.listen(expenseApplyMyListProvider(''), (prev, next) {
+        if (_firstBuild && !next.isLoading && !next.isReloading) {
+          setState(() => _firstBuild = false);
+          WidgetsBinding.instance.addPostFrameCallback((_) {
+            if (mounted) setState(() {});
+          });
+        }
+      });
+    }
+
     setNavBarTitle(context, ref, NavBarConfig(
       title: l10n.get('expenseApplyList'),
       showBack: true,
@@ -163,7 +166,7 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage>
             ])),
           ]),
         ),
-        Expanded(child: Container(color: colors.bgPage, child: _ExpenseApplyListContent(refreshCtrl: _refreshCtrl))),
+        Expanded(child: Container(color: colors.bgPage, child: _firstBuild ? const Center(child: SkeletonLoadingList()) : _ExpenseApplyListContent(refreshCtrl: _refreshCtrl, onRefresh: _doRefresh))),
       ]),
       floatingActionButton: FloatingActionButton(
         onPressed: () => context.push('/report/expense-apply-detail'),
@@ -177,14 +180,16 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage>
 
 class _ExpenseApplyListContent extends ConsumerWidget {
   final EasyRefreshController refreshCtrl;
-  const _ExpenseApplyListContent({required this.refreshCtrl});
+  final Future<void> Function() onRefresh;
+  const _ExpenseApplyListContent({required this.refreshCtrl, required this.onRefresh});
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
     final r = ResponsiveHelper.of(context);
     final itemsAsync = ref.watch(expenseApplyMyListProvider(''));
+    print('[TBOSS_OA][ExpenseApply] Content build: isLoading=${itemsAsync.isLoading}, hasValue=${itemsAsync.hasValue}, isReloading=${itemsAsync.isReloading}');
     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: () async { ref.read(expenseApplyRefreshProvider.notifier).state++; }, child: _buildContent(itemsAsync, context, ref))));
+    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) {

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

@@ -7,7 +7,7 @@ import '../../shared/widgets/empty_state.dart';
 import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../../shared/widgets/message_item.dart';
-import '../../shared/widgets/skeleton_list_card.dart';
+import '../../shared/widgets/app_skeletons.dart';
 import 'message_list_controller.dart';
 import 'message_model.dart';
 import '../../core/theme/app_colors.dart';

+ 29 - 23
lib/features/outing_log/outing_log_list_page.dart

@@ -9,7 +9,7 @@ import '../../core/utils/date_utils.dart' as du;
 import '../../core/utils/responsive.dart';
 import '../../shared/widgets/status_tag.dart';
 import '../../shared/widgets/empty_state.dart';
-import '../../shared/widgets/skeleton_list_card.dart';
+import '../../shared/widgets/app_skeletons.dart';
 import '../../shared/widgets/list_filter_panel.dart';
 import '../../shared/widgets/list_footer.dart';
 import '../../core/i18n/app_localizations.dart';
@@ -35,6 +35,8 @@ class _OutingLogListPageState extends ConsumerState<OutingLogListPage>
   ];
 
   late final TabController _tabCtrl;
+  bool _firstBuild = true;
+  bool _invalidateDone = false;
 
   @override
   void initState() {
@@ -45,6 +47,11 @@ class _OutingLogListPageState extends ConsumerState<OutingLogListPage>
         ref.read(outingLogTabProvider.notifier).state = _tabCtrl.index;
       }
     });
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      ref.invalidate(filteredOutingLogsProvider(0));
+      _invalidateDone = true;
+      setState(() {});
+    });
   }
 
   @override
@@ -55,6 +62,16 @@ class _OutingLogListPageState extends ConsumerState<OutingLogListPage>
 
   @override
   Widget build(BuildContext context) {
+    if (_invalidateDone) {
+      ref.listen(filteredOutingLogsProvider(0), (prev, next) {
+        if (_firstBuild && !next.isLoading && !next.isReloading) {
+          setState(() => _firstBuild = false);
+          WidgetsBinding.instance.addPostFrameCallback((_) {
+            if (mounted) setState(() {});
+          });
+        }
+      });
+    }
     final tabIndex = ref.watch(outingLogTabProvider);
     final dateStart = ref.watch(outingLogDateStartProvider);
     final dateEnd = ref.watch(outingLogDateEndProvider);
@@ -181,12 +198,14 @@ class _OutingLogListPageState extends ConsumerState<OutingLogListPage>
             Expanded(
               child: Container(
                 color: colors.bgPage,
-                child: TabBarView(
-                  controller: _tabCtrl,
-                  children: List.generate(_tabKeys.length, (tabIdx) {
-                    return _OutingLogTabContent(tabIndex: tabIdx);
-                  }),
-                ),
+                child: _firstBuild
+                    ? const Center(child: SkeletonLoadingList())
+                    : TabBarView(
+                        controller: _tabCtrl,
+                        children: List.generate(_tabKeys.length, (tabIdx) {
+                          return _OutingLogTabContent(tabIndex: tabIdx);
+                        }),
+                      ),
               ),
             ),
           ],
@@ -275,23 +294,10 @@ class _OutingLogTabContent extends ConsumerWidget {
     if (itemsAsync.isReloading) {
       final oldItems = itemsAsync.valueOrNull ?? [];
       if (oldItems.isEmpty) {
-        final message = tabIndex == 1
-            ? l10n.get('noDrafts')
-            : tabIndex == 2
-            ? l10n.get('noCompletedRecords')
-            : l10n.get('noOutingLogs');
-        return ListView(
-          children: [
-            const SizedBox(height: 120),
-            EmptyState(message: message),
-          ],
-        );
+        final message = tabIndex == 1 ? l10n.get('noDrafts') : tabIndex == 2 ? l10n.get('noCompletedRecords') : l10n.get('noOutingLogs');
+        return ListView(children: [const SizedBox(height: 120), EmptyState(message: message)]);
       }
-      return ListView.builder(
-        padding: const EdgeInsets.symmetric(vertical: 8),
-        itemCount: oldItems.length,
-        itemBuilder: (_, i) => _buildOutingLogItem(context, ref, oldItems[i]),
-      );
+      return ListView.builder(padding: const EdgeInsets.symmetric(vertical: 8), itemCount: oldItems.length, itemBuilder: (_, i) => _buildOutingLogItem(context, ref, oldItems[i]));
     }
 
     if (itemsAsync.hasError) {

+ 34 - 42
lib/features/overtime/overtime_list_page.dart

@@ -10,7 +10,7 @@ import '../../core/auth/role_provider.dart';
 import '../../shared/widgets/list_card.dart';
 import '../../shared/widgets/status_tag.dart';
 import '../../shared/widgets/empty_state.dart';
-import '../../shared/widgets/skeleton_list_card.dart';
+import '../../shared/widgets/app_skeletons.dart';
 import '../../shared/widgets/list_filter_panel.dart';
 import '../../shared/widgets/list_footer.dart';
 import '../../core/i18n/app_localizations.dart';
@@ -45,6 +45,8 @@ class _OvertimeListPageState extends ConsumerState<OvertimeListPage>
   ];
 
   late final TabController _tabCtrl;
+  bool _firstBuild = true;
+  bool _invalidateDone = false;
 
   @override
   void initState() {
@@ -56,6 +58,11 @@ class _OvertimeListPageState extends ConsumerState<OvertimeListPage>
             _tabKeys[_tabCtrl.index];
       }
     });
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      ref.invalidate(overtimeListProvider(''));
+      _invalidateDone = true;
+      setState(() {});
+    });
   }
 
   @override
@@ -66,6 +73,17 @@ class _OvertimeListPageState extends ConsumerState<OvertimeListPage>
 
   @override
   Widget build(BuildContext context) {
+    // 首次加载:invalidate 后数据到达才关闭骨架
+    if (_invalidateDone) {
+      ref.listen(overtimeListProvider(''), (prev, next) {
+        if (_firstBuild && !next.isLoading && !next.isReloading) {
+          setState(() => _firstBuild = false);
+          WidgetsBinding.instance.addPostFrameCallback((_) {
+            if (mounted) setState(() {});
+          });
+        }
+      });
+    }
     final status = ref.watch(overtimeStatusFilterProvider);
     final dateStart = ref.watch(overtimeDateStartProvider);
     final dateEnd = ref.watch(overtimeDateEndProvider);
@@ -221,12 +239,14 @@ class _OvertimeListPageState extends ConsumerState<OvertimeListPage>
         Expanded(
           child: Container(
             color: colors.bgPage,
-            child: TabBarView(
-              controller: _tabCtrl,
-              children: List.generate(_tabKeys.length, (tabIdx) {
-                return _buildTabContent(tabIdx);
-              }),
-            ),
+            child: _firstBuild
+                ? const Center(child: SkeletonLoadingList())
+                : TabBarView(
+                    controller: _tabCtrl,
+                    children: List.generate(_tabKeys.length, (tabIdx) {
+                      return _buildTabContent(tabIdx);
+                    }),
+                  ),
           ),
         ),
       ],
@@ -320,42 +340,14 @@ class _OvertimeTabContent extends ConsumerWidget {
     if (itemsAsync.isReloading) {
       final oldItems = itemsAsync.valueOrNull ?? [];
       if (oldItems.isEmpty) {
-        return ListView(
-          children: [
-            const SizedBox(height: 120),
-            EmptyState(message: l10n.get('noOvertimes')),
-          ],
-        );
+        return ListView(children: [const SizedBox(height: 120), EmptyState(message: l10n.get('noOvertimes'))]);
       }
-      return ListView.builder(
-        padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
-        itemCount: oldItems.length,
-        itemBuilder: (_, i) {
-          final desc = isSub
-              ? '${oldItems[i].otType} · ${oldItems[i].compensationType}\n申请人: ${oldItems[i].applicantName} · ${oldItems[i].deptName}'
-              : '${oldItems[i].otType} · ${oldItems[i].compensationType}';
-          final card = ListCard(
-            cardNo: oldItems[i].applicationNo,
-            description: desc,
-            amount:
-                '${oldItems[i].otHours.toStringAsFixed(1)}${l10n.get('hours')}',
-            amountColor: colors.textPrimary,
-            date: du.DateUtils.formatDate(oldItems[i].otDate),
-            statusTag: StatusTag.fromStatus(oldItems[i].status, l10n),
-            onTap: () => context.push('/overtime/detail/${oldItems[i].id}'),
-          );
-          if (isSub && oldItems[i].status == 'pending') {
-            return Padding(
-              padding: const EdgeInsets.only(bottom: 16),
-              child: _buildSwipeApprove(card, oldItems[i].id),
-            );
-          }
-          return Padding(
-            padding: const EdgeInsets.only(bottom: 16),
-            child: card,
-          );
-        },
-      );
+      return ListView.builder(padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), itemCount: oldItems.length, itemBuilder: (_, i) {
+        final desc = isSub ? '${oldItems[i].otType} · ${oldItems[i].compensationType}\n申请人: ${oldItems[i].applicantName} · ${oldItems[i].deptName}' : '${oldItems[i].otType} · ${oldItems[i].compensationType}';
+        final card = ListCard(cardNo: oldItems[i].applicationNo, description: desc, amount: '${oldItems[i].otHours.toStringAsFixed(1)}${l10n.get('hours')}', amountColor: colors.textPrimary, date: du.DateUtils.formatDate(oldItems[i].otDate), statusTag: StatusTag.fromStatus(oldItems[i].status, l10n), onTap: () => context.push('/overtime/detail/${oldItems[i].id}'));
+        if (isSub && oldItems[i].status == 'pending') return Padding(padding: const EdgeInsets.only(bottom: 16), child: _buildSwipeApprove(card, oldItems[i].id));
+        return Padding(padding: const EdgeInsets.only(bottom: 16), child: card);
+      });
     }
 
     if (itemsAsync.hasError) {

+ 32 - 36
lib/features/vehicle/vehicle_list_page.dart

@@ -9,7 +9,7 @@ import '../../core/auth/role_provider.dart';
 import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../shared/widgets/empty_state.dart';
-import '../../shared/widgets/skeleton_list_card.dart';
+import '../../shared/widgets/app_skeletons.dart';
 import '../../shared/widgets/list_filter_panel.dart';
 import '../../shared/widgets/list_footer.dart';
 import '../../shared/widgets/status_tag.dart';
@@ -47,6 +47,8 @@ class _VehicleListPageState extends ConsumerState<VehicleListPage>
   ];
 
   late final TabController _tabCtrl;
+  bool _firstBuild = true;
+  bool _invalidateDone = false;
 
   @override
   void initState() {
@@ -58,6 +60,11 @@ class _VehicleListPageState extends ConsumerState<VehicleListPage>
             _tabKeys[_tabCtrl.index];
       }
     });
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      ref.invalidate(vehicleListProvider(''));
+      _invalidateDone = true;
+      setState(() {});
+    });
   }
 
   @override
@@ -68,6 +75,16 @@ class _VehicleListPageState extends ConsumerState<VehicleListPage>
 
   @override
   Widget build(BuildContext context) {
+    if (_invalidateDone) {
+      ref.listen(vehicleListProvider(''), (prev, next) {
+        if (_firstBuild && !next.isLoading && !next.isReloading) {
+          setState(() => _firstBuild = false);
+          WidgetsBinding.instance.addPostFrameCallback((_) {
+            if (mounted) setState(() {});
+          });
+        }
+      });
+    }
     final status = ref.watch(vehicleStatusFilterProvider);
     final dateStart = ref.watch(vehicleDateStartProvider);
     final dateEnd = ref.watch(vehicleDateEndProvider);
@@ -217,12 +234,14 @@ class _VehicleListPageState extends ConsumerState<VehicleListPage>
         Expanded(
           child: Container(
             color: colors.bgPage,
-            child: TabBarView(
-              controller: _tabCtrl,
-              children: List.generate(_tabKeys.length, (tabIdx) {
-                return _buildTabContent(tabIdx);
-              }),
-            ),
+            child: _firstBuild
+                ? const Center(child: SkeletonLoadingList())
+                : TabBarView(
+                    controller: _tabCtrl,
+                    children: List.generate(_tabKeys.length, (tabIdx) {
+                      return _buildTabContent(tabIdx);
+                    }),
+                  ),
           ),
         ),
       ],
@@ -316,35 +335,12 @@ class _VehicleTabContent extends ConsumerWidget {
     final isSub = scope == 'sub';
     if (itemsAsync.isReloading) {
       final oldItems = itemsAsync.valueOrNull ?? [];
-      if (oldItems.isEmpty) {
-        return ListView(
-          children: [
-            const SizedBox(height: 120),
-            EmptyState(message: l10n.get('noVehicles')),
-          ],
-        );
-      }
-      return ListView.builder(
-        padding: const EdgeInsets.all(16),
-        itemCount: oldItems.length,
-        itemBuilder: (_, i) {
-          final card = _buildVehicleListItem(
-            context,
-            oldItems[i],
-            isSub: isSub,
-          );
-          if (isSub && oldItems[i].status == 'pending') {
-            return Padding(
-              padding: const EdgeInsets.only(bottom: 16),
-              child: _buildSwipeApprove(card, oldItems[i].id),
-            );
-          }
-          return Padding(
-            padding: const EdgeInsets.only(bottom: 16),
-            child: card,
-          );
-        },
-      );
+      if (oldItems.isEmpty) return ListView(children: [const SizedBox(height: 120), EmptyState(message: l10n.get('noVehicles'))]);
+      return ListView.builder(padding: const EdgeInsets.all(16), itemCount: oldItems.length, itemBuilder: (_, i) {
+        final card = _buildVehicleListItem(context, oldItems[i], isSub: isSub);
+        if (isSub && oldItems[i].status == 'pending') return Padding(padding: const EdgeInsets.only(bottom: 16), child: _buildSwipeApprove(card, oldItems[i].id));
+        return Padding(padding: const EdgeInsets.only(bottom: 16), child: card);
+      });
     }
 
     if (itemsAsync.hasError) {

+ 138 - 0
lib/shared/widgets/skeleton_list_card.dart → lib/shared/widgets/app_skeletons.dart

@@ -309,3 +309,141 @@ class SkeletonMessageCard extends StatelessWidget {
     );
   }
 }
+
+/// 详情页骨架(ExpenseDetailPage / ExpenseApplyDetailPage)
+/// 匹配 FormSection 卡片布局:基本信息 + 明细 + 附件
+class SkeletonDetailPage extends StatelessWidget {
+  const SkeletonDetailPage({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return SingleChildScrollView(
+      padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
+      child: Skeletonizer(
+        enabled: true,
+        child: Column(
+          children: [
+            _FormCard(rows: 7),
+            const SizedBox(height: 16),
+            _FormCard(rows: 2),
+            const SizedBox(height: 16),
+            _FormCard(rows: 3),
+            const SizedBox(height: 24),
+            const Center(child: Bone(width: 120, height: 14)),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+/// 表单页骨架(ExpenseApplyCreatePage / ExpenseCreatePage)
+/// 匹配 FormSection + 输入框 + 底部操作栏布局
+class SkeletonFormPage extends StatelessWidget {
+  final bool showImportLink;
+  const SkeletonFormPage({super.key, this.showImportLink = false});
+
+  @override
+  Widget build(BuildContext context) {
+    final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final bottomPadding = MediaQuery.of(context).padding.bottom;
+    return Stack(
+      children: [
+        SingleChildScrollView(
+          padding: EdgeInsets.fromLTRB(16, 16, 16, 80 + bottomPadding),
+          child: Skeletonizer(
+            enabled: true,
+            child: Column(
+              children: [
+                if (showImportLink) ...[
+                  // 导入申请单按钮骨架
+                  Container(
+                    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),
+                    ]),
+                  ),
+                  const SizedBox(height: 16),
+                ],
+                _FormCard(rows: 5),
+                const SizedBox(height: 16),
+                _FormCard(rows: 2),
+                const SizedBox(height: 16),
+                _FormCard(rows: 3),
+                const SizedBox(height: 24),
+                const Center(child: Bone(width: 120, height: 14)),
+              ],
+            ),
+          ),
+        ),
+        // 底部操作栏骨架,贴底并适配安全区
+        Positioned(
+          left: 0, right: 0, bottom: 0,
+          child: Skeletonizer(
+            enabled: true,
+            child: Container(
+              padding: EdgeInsets.fromLTRB(16, 12, 16, 12 + bottomPadding),
+              decoration: BoxDecoration(
+                color: colors.bgCard,
+                border: Border(top: BorderSide(color: colors.border)),
+              ),
+              child: Row(
+                children: [
+                  Expanded(child: Bone(height: 44, borderRadius: BorderRadius.circular(8))),
+                  const SizedBox(width: 12),
+                  Expanded(child: Bone(height: 44, borderRadius: BorderRadius.circular(8))),
+                ],
+              ),
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+}
+
+class _FormCard extends StatelessWidget {
+  final int rows;
+  const _FormCard({required this.rows});
+
+  @override
+  Widget build(BuildContext context) {
+    final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    return 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),
+            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),
+              ],
+            ),
+          )),
+        ],
+      ),
+    );
+  }
+}

+ 42 - 13
lib/shared/widgets/nav_bar_config.dart

@@ -1,5 +1,14 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:go_router/go_router.dart';
+
+/// 按 BuildContext 缓存 token:同 State 多次 build 复用同一 token,
+/// 新 State(context 对象不同)自动分配新 token,确保 onBack 闭包不被旧 State 覆盖
+final Expando<int> _contextToken = Expando<int>();
+int _globalNavToken = 0;
+
+int _tokenForContext(BuildContext context) =>
+    _contextToken[context] ??= ++_globalNavToken;
 
 /// NavBar 配置,由各页面在 build 中设置,AppShell 统一渲染
 class NavBarConfig {
@@ -11,6 +20,9 @@ class NavBarConfig {
   final IconData? leadingIcon;
   final bool hasFilter;
   final int filterVersion;
+  /// 页面实例标识:每个 State 实例生成唯一 token,确保新 State 的 onBack 闭包不被
+  /// 旧 State 的 config(值相等但闭包指向已销毁 State)覆盖
+  final int? token;
 
   const NavBarConfig({
     required this.title,
@@ -21,6 +33,7 @@ class NavBarConfig {
     this.leadingIcon,
     this.hasFilter = false,
     this.filterVersion = 0,
+    this.token,
   });
 
   /// 根页面默认配置(无返回按钮)
@@ -55,12 +68,13 @@ class NavBarConfig {
       other.showBack == showBack &&
       other.showRight == showRight &&
       other.hasFilter == hasFilter &&
-      other.filterVersion == filterVersion;
-  // 故意不比较 onBack / rightWidget / leadingIcon,避免每次 build 新闭包触发无限重建
+      other.filterVersion == filterVersion &&
+      other.token == token;
+  // token 确保不同 State 实例的 config 不相等,onBack/rightWidget 通过 token 间接区分
 
   @override
   int get hashCode =>
-      Object.hash(title, showBack, showRight, hasFilter, filterVersion);
+      Object.hash(title, showBack, showRight, hasFilter, filterVersion, token);
 }
 
 /// NavBar 配置变更器,内部做相等判断避免无限重建
@@ -85,17 +99,32 @@ final navBarConfigProvider =
       (ref) => NavBarConfigNotifier(),
     );
 
-/// 仅在当前路由可见时更新导航栏标题,避免被覆盖的页面因 GoRouter
-/// rebuild 级联效应竞争标题(如 showDatePicker 导致的路由栈全量重建)。
+/// 更新导航栏配置。使用 GoRouter 判断当前页面是否为可见路由:
+/// - 页面自身的 GoRouterState.uri == GoRouter 注册的当前 uri → 允许更新
+/// - 不匹配 → 被覆盖的旧页面(GoRouter 级联重建导致),跳过
 ///
-/// [context] 为当前页面 build 的 context,
-/// [config] 为要设置的 NavBarConfig
+/// 此方法替代了 ModalRoute.isCurrent,因为缓存引擎下旧路由残留会导致
+/// ModalRoute 误判;GoRouter 的状态不受 Widget 树生命周期影响,结果更可靠
 void setNavBarTitle(BuildContext context, WidgetRef ref, NavBarConfig config) {
-  final route = ModalRoute.of(context);
-  // null = 首次 build 路由尚未完全插入,仍需更新
-  // isCurrent = 当前可见路由
-  // false = 已被覆盖的旧页面,跳过
-  if (route == null || route.isCurrent) {
-    ref.read(navBarConfigProvider.notifier).update(config);
+  try {
+    final pageUri = GoRouterState.of(context).uri.toString();
+    final currentUri = GoRouter.of(context).state.uri.toString();
+    if (pageUri != currentUri) return;
+  } catch (_) {
+    // GoRouter 不可用时直接放行
   }
+  // 注入 context 绑定的 token:同 State 多次 build 共享,换 State 自动换新 token
+  final token = _tokenForContext(context);
+  final effective = config.token != null ? config : NavBarConfig(
+    title: config.title,
+    showBack: config.showBack,
+    showRight: config.showRight,
+    rightWidget: config.rightWidget,
+    onBack: config.onBack,
+    leadingIcon: config.leadingIcon,
+    hasFilter: config.hasFilter,
+    filterVersion: config.filterVersion,
+    token: token,
+  );
+  ref.read(navBarConfigProvider.notifier).update(effective);
 }