import 'dart:math'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../../core/router/routes.dart'; import '../../core/constants/app_colors.dart'; import '../../core/updater/update_service.dart'; import '../../core/updater/channels/self_hosted_updater.dart'; class MainShell extends StatefulWidget { final Widget child; const MainShell({super.key, required this.child}); @override State createState() => _MainShellState(); } class _MainShellState extends State with WidgetsBindingObserver { /// 下次允许检查更新的时间(static 防止 widget rebuild 重置) static DateTime? _nextCheckAllowedTime; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addPostFrameCallback((_) { _checkForUpdateIfNeeded(); }); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { _checkForUpdateIfNeeded(); } } Future _checkForUpdateIfNeeded() async { final now = DateTime.now(); // 如果从未检查过,或者已过冷却时间,则检查 if (_nextCheckAllowedTime == null || now.isAfter(_nextCheckAllowedTime!)) { // 设置下次允许检查时间(90-300秒随机间隔) final randomSeconds = 90 + Random().nextInt(211); _nextCheckAllowedTime = now.add(Duration(seconds: randomSeconds)); // 延迟3秒,避免启动时干扰用户 await Future.delayed(const Duration(seconds: 3)); if (!mounted) return; try { final result = await UpdateService.instance.checkForUpdate(); if (!mounted) return; if (result.hasUpdate && result.versionInfo != null) { await SelfHostedUpdater.show( context, versionInfo: result.versionInfo!, isForceUpdate: result.versionInfo!.isForceUpdate, ); } } catch (e) { debugPrint('[MainShell] 检查更新失败: $e'); } } } @override Widget build(BuildContext context) { final isDark = AppColors.isDark(context); return Scaffold( body: widget.child, bottomNavigationBar: Container( decoration: BoxDecoration( color: AppColors.cardOf(context), boxShadow: [ BoxShadow( color: isDark ? Colors.black.withOpacity(0.3) : Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, -2), ), ], ), child: SafeArea( top: false, child: Container( height: 56, padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildNavItem( context, icon: Icons.favorite_outline, activeIcon: Icons.favorite, label: '贡献值', index: 0, ), _buildNavItem( context, icon: Icons.swap_horiz_outlined, activeIcon: Icons.swap_horiz, label: '兑换', index: 1, ), _buildNavItem( context, icon: Icons.account_balance_wallet_outlined, activeIcon: Icons.account_balance_wallet, label: '资产', index: 2, ), _buildNavItem( context, icon: Icons.person_outline, activeIcon: Icons.person, label: '我的', index: 3, ), ], ), ), ), ), ); } Widget _buildNavItem( BuildContext context, { required IconData icon, required IconData activeIcon, required String label, required int index, }) { final currentIndex = _calculateSelectedIndex(context); final isSelected = currentIndex == index; final inactiveColor = AppColors.textMutedOf(context); return GestureDetector( onTap: () => _onItemTapped(index, context), behavior: HitTestBehavior.opaque, child: SizedBox( width: 64, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( isSelected ? activeIcon : icon, size: 24, color: isSelected ? AppColors.orange : inactiveColor, ), const SizedBox(height: 4), Text( label, style: TextStyle( fontSize: 11, fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, color: isSelected ? AppColors.orange : inactiveColor, ), ), ], ), ), ); } int _calculateSelectedIndex(BuildContext context) { final location = GoRouterState.of(context).uri.path; if (location.startsWith(Routes.contribution)) return 0; if (location.startsWith(Routes.trading)) return 1; if (location.startsWith(Routes.asset)) return 2; if (location.startsWith(Routes.profile)) return 3; return 0; } void _onItemTapped(int index, BuildContext context) { switch (index) { case 0: context.go(Routes.contribution); break; case 1: context.go(Routes.trading); break; case 2: context.go(Routes.asset); break; case 3: context.go(Routes.profile); break; } } }