import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:get_it/get_it.dart'; import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_config_cubit.dart'; import 'package:maibu_satabot_v2/features/main_container/domain/tab_config.dart'; import 'package:maibu_satabot_v2/features/home/presentation/pages/home_page.dart'; import 'package:maibu_satabot_v2/features/v2/home/presentation/pages/home_v2_page.dart'; import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_bloc.dart'; // 旧设备页保留备用:如需回退,换回 DeviceStatusPage 并恢复下方 import。 // import 'package:maibu_satabot_v2/features/v2/device_list/presentation/pages/device_status_page.dart'; import 'package:maibu_satabot_v2/features/v2/device_list/presentation/pages/device_status_page_v2.dart'; import 'package:maibu_satabot_v2/features/ai/presentation/pages/ai_page.dart'; import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/pages/alarm_center_page.dart'; import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/bloc/alarm_cubit.dart'; import 'package:maibu_satabot_v2/features/my/presentation/pages/my_page.dart'; import 'package:maibu_satabot_v2/features/v2/my/presentation/pages/my_page.dart' as my_v2; // 新版「我的」页(M01 设计稿);旧版 my_page.dart 保留,如需回退换回 my_v2.MyPage 即可。 import 'package:maibu_satabot_v2/features/v2/my/presentation/pages/my_page_v2.dart' as my_v2_new; import 'package:maibu_satabot_v2/features/v2/workorder/presentation/pages/workorder_page.dart'; import 'package:maibu_satabot_v2/features/v2/report/presentation/pages/report_page.dart'; import 'package:maibu_satabot_v2/features/v2/work/presentation/pages/work_home_page_v2.dart'; import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/view/float_bar_widget.dart'; import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart'; import 'package:maibu_satabot_v2/core/di/injection.dart'; import 'package:maibu_satabot_v2/core/theme/AppTheme.dart'; import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart'; /// 底部悬浮导航占据的高度(不含系统手势条安全区): /// 上下 Padding 16×2 + 胶囊内上下 padding 2×2 + 胶囊内容高 56 = 92 /// 页面内容需要在这个高度之上收住,否则会被悬浮导航盖住 const double kBottomNavDock = 92; class MainWrapper extends StatefulWidget { const MainWrapper({super.key}); @override State createState() => _MainWrapperState(); } class _MainWrapperState extends State { int _currentIndex = 0; final PageController _pageController = PageController(); @override void initState() { super.initState(); // 🔥 确保 FloatBarSettingService 已初始化 GetIt.I(); } @override void dispose() { _pageController.dispose(); super.dispose(); } void _onTabChanged(int index) { setState(() { _currentIndex = index; }); _pageController.jumpToPage(index); } /// Tab 图标:选中用 filled(实心),未选用 outlined(描边),对比更清晰 IconData _navIcon(String iconName, bool selected) { switch (iconName) { case 'home_rounded': return selected ? Icons.home_rounded : Icons.home_outlined; case 'grid_view_rounded': return selected ? Icons.grid_view_rounded : Icons.grid_view_outlined; case 'map_rounded': return selected ? Icons.map_rounded : Icons.map_outlined; case 'devices_rounded': return selected ? Icons.devices_rounded : Icons.devices_outlined; case 'auto_awesome_rounded': return selected ? Icons.auto_awesome_rounded : Icons.auto_awesome_outlined; case 'warning_amber_rounded': return selected ? Icons.warning_amber_rounded : Icons.warning_amber_outlined; case 'assignment_rounded': return selected ? Icons.assignment_rounded : Icons.assignment_outlined; case 'post_add_rounded': return selected ? Icons.post_add_rounded : Icons.post_add_outlined; case 'person_rounded': return selected ? Icons.person_rounded : Icons.person_outlined; default: return selected ? Icons.home_rounded : Icons.home_outlined; } } Widget _buildCurrentPage( TabConfigItem tab, List enabledItems, ) { switch (tab.id) { case 'home_v2': return BlocProvider( create: (_) => sl(), child: const HomeV2Page(), ); case 'home': return const HomePage(); case 'device': return const DeviceStatusPageV2(); // 作业首页(W01):返回键切回「设备」tab case 'work': return WorkHomePageV2( isActive: enabledItems[_currentIndex].id == tab.id, onBack: () { final int deviceIndex = enabledItems.indexWhere( (item) => item.id == 'device', ); _onTabChanged(deviceIndex >= 0 ? deviceIndex : 0); }, ); case 'ai': return const AiPage(); case 'warning': return BlocProvider( create: (_) => sl(), child: const AlarmCenterPage(), ); case 'workorder': return const WorkOrderPage(); case 'report': return const ReportPage(); case 'my': return const MyPage(); case 'me': return const my_v2_new.MyPageV2(); default: return const HomePage(); } } @override Widget build(BuildContext context) { // 🔥 个人角色(roleKey == 'personal')底部导航强制只显示「设备 + 我的」 final roleKey = context.watch().state.user?.roleKey; return BlocBuilder( builder: (context, state) { if (state is! TabConfigLoaded) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); } final config = state.config; final enabledItems = config.navItemsForRole(roleKey); if (enabledItems.isEmpty) { return const Scaffold(body: Center(child: Text('请至少启用一个 Tab'))); } if (_currentIndex >= enabledItems.length) { _currentIndex = 0; } // 键盘弹出时 body 已被 Scaffold 缩短到底栏之上,无需再叠加导航高度 final mq = MediaQuery.of(context); final keyboardOpen = mq.viewInsets.bottom > 0; return Scaffold( // 底部导航改为「真正悬浮」:body 铺满到屏幕底部(含手势条区域), // 这样底栏区域不再被 Scaffold 的 pageBackground 盖成一条白带, // 页面自身的背景(背景图/渐变)可以一直延伸到底。 extendBody: true, backgroundColor: context.appColors.pageBackground, // 向 body 子树注入「底部导航高度」:使用 SafeArea 的页面会自动让位, // 且 SafeArea 内部会 removePadding,嵌套不会重复叠加。 body: MediaQuery( data: mq.copyWith( padding: mq.padding.copyWith( bottom: mq.padding.bottom + (keyboardOpen ? 0 : kBottomNavDock), ), ), child: Stack( children: [ PageView( controller: _pageController, // 🔥 启用左右滑动切换 Tab physics: const BouncingScrollPhysics(), onPageChanged: (index) { setState(() { _currentIndex = index; }); }, children: enabledItems .map((tab) => _buildCurrentPage(tab, enabledItems)) .toList(), ), // 悬浮条 - 可拖动的悬浮按钮 ValueListenableBuilder( valueListenable: FloatBarSettingService.settingNotifier, builder: (context, isEnabled, child) { return Visibility( visible: isEnabled, child: const FloatBarWidget(), ); }, ), ], ), ), bottomNavigationBar: SafeArea( top: false, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), child: LayoutBuilder( builder: (context, constraints) { final itemCount = enabledItems.length; const double barPaddingH = 6; final double maxBarWidth = constraints.maxWidth; // 3 个及以上 Tab 直接铺满可用宽度:每格自然等分(3 个时约 108/格), // 不会像固定 84/格 那样缩成一小段胶囊; // 只有 2 个 Tab(如个人角色:设备 + 我的)才收窄居中,避免太空旷 final double barWidth = itemCount >= 3 ? maxBarWidth : (itemCount * 116 < maxBarWidth ? itemCount * 116 : maxBarWidth); final double tabWidth = (barWidth - barPaddingH * 2) / itemCount; // 选中高亮:包裹「图标 + 文字」的紧凑胶囊 chip const double indicatorH = 46; // 上限放到 68:3 个 Tab 时每格约 108,胶囊跟着变宽才不显巴掌小; // 仍保留 tabWidth - 8 的下限,多 Tab 时不会溢出格子 final double indicatorW = tabWidth - 8 < 68 ? tabWidth - 8 : 68.0; // 用 Align + heightFactor 只水平居中,高度贴合内容; // 切勿用 Center:它在底部栏有界高度约束下会撑满全屏,把 body 挤没 return Align( alignment: Alignment.center, heightFactor: 1.0, // 底栏不填底、不投外阴影,只留一圈淡蓝细边: // 能看出它自己是个独立的小胶囊,但不会变成一个「盒子」 child: Container( width: barWidth, decoration: BoxDecoration( borderRadius: BorderRadius.circular(30), border: Border.all( color: context.appColors.primary.withValues( alpha: 0.18, ), ), ), child: Padding( padding: const EdgeInsets.symmetric( horizontal: barPaddingH, vertical: 2, ), child: SizedBox( height: 56, child: Stack( children: [ // 蓝色胶囊 chip:纯数学定位,居中包裹图标 + 文字 AnimatedPositioned( duration: const Duration(milliseconds: 280), curve: Curves.easeOutCubic, left: _currentIndex * tabWidth + (tabWidth - indicatorW) / 2, top: (56 - indicatorH) / 2, width: indicatorW, height: indicatorH, child: Container( decoration: BoxDecoration( color: context.appColors.primary .withValues(alpha: 0.12), borderRadius: BorderRadius.circular( indicatorH / 2, ), // 只给选中胶囊一层轻投影,让它自己浮起来 boxShadow: [ BoxShadow( color: context.appColors.primary .withValues(alpha: 0.18), blurRadius: 12, offset: const Offset(0, 4), ), ], ), ), ), // Tab 项 Row( children: enabledItems.asMap().entries.map((entry) { final index = entry.key; final item = entry.value; final isSelected = _currentIndex == index; return Expanded( child: InkWell( borderRadius: BorderRadius.circular( indicatorH / 2, ), onTap: () => _onTabChanged(index), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ AnimatedScale( scale: isSelected ? 1.1 : 1.0, duration: const Duration( milliseconds: 200, ), curve: Curves.easeOutBack, child: Icon( _navIcon(item.icon, isSelected), size: 22, color: isSelected ? context.appColors.primary : context .appColors.textTertiary, ), ), const SizedBox(height: 2), AnimatedDefaultTextStyle( duration: const Duration( milliseconds: 200, ), style: TextStyle( fontSize: 11, height: 1.0, color: isSelected ? context.appColors.primary : context .appColors.textTertiary, fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, ), child: Text( // 按当前语言取 Tab 名:en 用 nameEn,其余用 name // 通过 Localizations.localeOf 注册依赖,语言切换时底部导航随之重建 Localizations.localeOf( context, ).languageCode == 'en' ? item.nameEn : item.name, ), ), ], ), ), ); }).toList(), ), ], ), ), ), ), ); }, ), ), ), ); }, ); } }