import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'core/app_router.dart'; import 'core/app_theme.dart'; import 'core/navigation_provider.dart'; import 'pages/splash_page.dart'; import 'providers/auth_provider.dart'; import 'providers/data_providers.dart'; /// 健康管家 App 根组件 class HealthApp extends ConsumerWidget { const HealthApp({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { return MaterialApp( title: '小脉健康', debugShowCheckedModeBanner: false, theme: AppTheme.lightTheme, localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], supportedLocales: const [Locale('zh', 'CN'), Locale('zh')], locale: const Locale('zh'), home: const _RootNavigator(), // 注入 ShadTheme + 启动闸门:Splash 盖在最上层,直到首页今日健康卡数据就绪 // 外层包一层 GestureDetector:点击任意空白处取消输入框焦点(全 app 生效) builder: (context, child) => ShadTheme( data: AppTheme.shadTheme, child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () => FocusManager.instance.primaryFocus?.unfocus(), child: _BootGate(child: child!), ), ), ); } } /// 启动是否就绪:auth 判定完成,且(已登录的普通用户)今日健康卡数据已到位。 /// 未登录或医生/管理员无需等待健康卡。 final appReadyProvider = Provider((ref) { final auth = ref.watch(authProvider); if (auth.isLoading) return false; // 还在判登录态 → 继续盖 Splash if (!auth.isLoggedIn) return true; // 未登录 → 就绪,露出登录页 final currentRoute = ref.watch(routeStackProvider).last.name; if (currentRoute == 'login') return false; // 等根导航切到已登录首页后再撤 Splash final role = auth.user?.role ?? 'User'; if (role != 'User') return true; // 医生/管理员首页不依赖今日健康卡 // 普通用户:等今日健康卡片数据(成功或失败都算就绪,避免卡死) final health = ref.watch(latestHealthProvider); return health.hasValue || health.hasError; }); /// 启动闸门——就绪前在最上层覆盖 Splash,盖住登录页闪现与首页对话流初始态。 /// 带 8 秒安全兜底,避免离线时数据永不返回导致卡在启动页。 class _BootGate extends ConsumerStatefulWidget { final Widget child; const _BootGate({required this.child}); @override ConsumerState<_BootGate> createState() => _BootGateState(); } class _BootGateState extends ConsumerState<_BootGate> { bool _timedOut = false; Timer? _timer; @override void initState() { super.initState(); _startTimeout(); } void _startTimeout() { _timer?.cancel(); _timer = Timer(const Duration(seconds: 8), () { if (mounted) setState(() => _timedOut = true); }); } void _handleReadyChanged(bool? previous, bool ready) { if (ready) { _timer?.cancel(); if (_timedOut) setState(() => _timedOut = false); return; } if (previous != false) { if (_timedOut) setState(() => _timedOut = false); _startTimeout(); } } @override void dispose() { _timer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { ref.listen(appReadyProvider, _handleReadyChanged); final ready = ref.watch(appReadyProvider) || _timedOut; return Stack( children: [ widget.child, if (!ready) const Positioned.fill(child: SplashPage()), ], ); } } /// 根导航——根据 Riverpod 路由状态切换页面 class _RootNavigator extends ConsumerWidget { const _RootNavigator(); @override Widget build(BuildContext context, WidgetRef ref) { final stack = ref.watch(routeStackProvider); final current = stack.last; final authState = ref.watch(authProvider); final isPublicStaticText = current.name == 'staticText'; // 登录后自动跳转(在下一帧完成,无闪烁) if (authState.isLoggedIn && current.name == 'login') { WidgetsBinding.instance.addPostFrameCallback((_) { ref.invalidate(latestHealthProvider); ref.invalidate(medicationListProvider); ref.invalidate(medicationReminderProvider); ref.invalidate(currentExercisePlanProvider); final role = authState.user?.role ?? 'User'; if (role == 'Admin') { goRoute(ref, 'adminHome'); } else if (role == 'Doctor') { goRoute(ref, 'doctorHome'); } else { goRoute(ref, 'home'); } }); } if (!authState.isLoading && !authState.isLoggedIn && current.name != 'login' && !isPublicStaticText) { WidgetsBinding.instance.addPostFrameCallback((_) { goRoute(ref, 'login'); }); } return PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) { if (didPop) return; if (stack.length > 1) { popRoute(ref); return; } final homeRoute = switch (authState.user?.role) { 'Admin' => 'adminHome', 'Doctor' => 'doctorHome', _ => 'home', }; if (current.name != homeRoute && authState.isLoggedIn) { goRoute(ref, homeRoute); } }, child: Navigator( pages: [ for (var index = 0; index < stack.length; index++) MaterialPage( key: ValueKey(_pageKey(stack[index], index)), name: stack[index].name, child: buildPage(stack[index], ref), ), ], onDidRemovePage: (page) { final routes = ref.read(routeStackProvider); if (routes.length <= 1) return; final lastIndex = routes.length - 1; final currentPageKey = ValueKey(_pageKey(routes.last, lastIndex)); if (page.key == currentPageKey) popRoute(ref); }, ), ); } String _pageKey(RouteInfo route, int index) { final params = route.params.entries .map((entry) => '${entry.key}=${entry.value}') .join('&'); return '$index:${route.name}:$params'; } }