Files
flutterApp/lib/features/main_container/presentation/main_wrapper.dart

372 lines
17 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<MainWrapper> createState() => _MainWrapperState();
}
class _MainWrapperState extends State<MainWrapper> {
int _currentIndex = 0;
final PageController _pageController = PageController();
@override
void initState() {
super.initState();
// 🔥 确保 FloatBarSettingService 已初始化
GetIt.I<FloatBarSettingService>();
}
@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<TabConfigItem> enabledItems,
) {
switch (tab.id) {
case 'home_v2':
return BlocProvider(
create: (_) => sl<HomeV2Bloc>(),
child: const HomeV2Page(),
);
case 'home':
return const HomePage();
case 'device':
return const DeviceStatusPageV2();
// 作业首页(W01):返回键切回「设备」tab
case 'work':
return WorkHomePageV2(
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<AlarmCubit>(),
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<AppUserCubit>().state.user?.roleKey;
return BlocBuilder<TabConfigCubit, TabConfigState>(
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<bool>(
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(),
),
],
),
),
),
),
);
},
),
),
),
);
},
);
}
}