import 'package:flutter/material.dart'; import '../app/theme/app_colors.dart'; import '../app/i18n/app_localizations.dart'; import '../core/updater/update_service.dart'; import '../core/providers/notification_badge_manager.dart'; import '../features/coupons/presentation/pages/home_page.dart'; import '../features/coupons/presentation/pages/market_page.dart'; import '../features/message/presentation/pages/message_page.dart'; import '../features/profile/presentation/pages/profile_page.dart'; /// GenexMobile主Shell - Bottom Navigation /// /// Tab: 首页 / 交易 / 消息 / 我的 class MainShell extends StatefulWidget { const MainShell({super.key}); @override State createState() => _MainShellState(); } /// 混入 WidgetsBindingObserver 以监听 App 生命周期变化(前后台切换) class _MainShellState extends State with WidgetsBindingObserver { int _currentIndex = 0; /// 首次进入 Shell 是否已触发过更新检查(防止 didChangeDependencies 多次调用) bool _updateChecked = false; /// 上一次执行更新检查的时间(用于节流,避免短时间内重复检查) DateTime? _lastUpdateCheck; /// 本次 App 生命周期(冷启动到下次冷启动)是否已向用户展示过更新弹窗。 /// /// 设计逻辑: /// - 若检查到新版本并弹窗,此标志置为 true,之后从后台切回不再重复弹窗, /// 避免用户选择"稍后再说"后频繁打扰。 /// - 标志仅存活于内存,App 被彻底杀死并重启后自动重置,届时再次检查。 bool _updatePromptShown = false; final _pages = const [ HomePage(), MarketPage(), MessagePage(), ProfilePage(), ]; @override void initState() { super.initState(); // 注册生命周期观察者,以便在 didChangeAppLifecycleState 中感知前后台切换 WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeDependencies() { super.didChangeDependencies(); // 冷启动后仅执行一次:等待 3 秒页面稳定后检查更新 if (!_updateChecked) { _updateChecked = true; _checkForUpdate(); } } /// App 从后台切换回前台时触发 @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { // 本次 session 已弹过更新弹窗(用户选择了"稍后"),不再重复打扰, // 等用户下次冷启动 App 时才会再次检查 if (_updatePromptShown) return; final now = DateTime.now(); // 节流:两次后台唤起检查之间至少间隔 2 分钟,防止频繁切换时多次触发网络请求 if (_lastUpdateCheck == null || now.difference(_lastUpdateCheck!) > const Duration(minutes: 2)) { _checkForUpdate(); } } } /// 执行静默版本检查,有新版本时才弹更新弹窗 /// /// 流程: /// 1. 记录本次检查时间(用于节流) /// 2. 延迟 3 秒,确保页面已完全渲染 /// 3. 先调用 silentCheck() 查询服务端是否有新版本(不弹窗) /// 4. 若有新版本,标记 _updatePromptShown = true 后调用 checkForUpdate() 弹窗 /// 若无新版本,静默结束,下次切回前台满 2 分钟后再次检查 Future _checkForUpdate() async { _lastUpdateCheck = DateTime.now(); await Future.delayed(const Duration(seconds: 3)); if (!mounted) return; final updateService = UpdateService(); final versionInfo = await updateService.silentCheck(); if (!mounted) return; if (versionInfo != null) { // 有新版本:先锁定标志,再弹窗(无论用户选更新还是关闭,本 session 均不再弹) _updatePromptShown = true; await updateService.checkForUpdate(context); } } @override Widget build(BuildContext context) { return Scaffold( body: IndexedStack( index: _currentIndex, children: _pages, ), bottomNavigationBar: Container( decoration: const BoxDecoration( color: AppColors.surface, border: Border(top: BorderSide(color: AppColors.borderLight, width: 0.5)), ), child: NavigationBar( selectedIndex: _currentIndex, onDestinationSelected: (index) => setState(() => _currentIndex = index), destinations: [ _buildDestination(Icons.home_rounded, Icons.home_outlined, context.t('nav.home')), _buildDestination(Icons.show_chart_rounded, Icons.show_chart_outlined, context.t('nav.trading')), _buildBadgeDestination( Icons.notifications_rounded, Icons.notifications_outlined, context.t('nav.messages'), ), _buildDestination(Icons.person_rounded, Icons.person_outlined, context.t('nav.profile')), ], ), ), ); } NavigationDestination _buildDestination( IconData selected, IconData unselected, String label, ) { return NavigationDestination( icon: Icon(unselected), selectedIcon: Icon(selected), label: label, ); } NavigationDestination _buildBadgeDestination( IconData selected, IconData unselected, String label, ) { return NavigationDestination( icon: ValueListenableBuilder( valueListenable: NotificationBadgeManager().unreadCount, builder: (context, count, _) { if (count <= 0) return Icon(unselected); return Badge( label: Text('$count', style: const TextStyle(fontSize: 10)), child: Icon(unselected), ); }, ), selectedIcon: ValueListenableBuilder( valueListenable: NotificationBadgeManager().unreadCount, builder: (context, count, _) { if (count <= 0) return Icon(selected); return Badge( label: Text('$count', style: const TextStyle(fontSize: 10)), child: Icon(selected), ); }, ), label: label, ); } }