app_shell.dart 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. import 'package:flutter_riverpod/flutter_riverpod.dart';
  4. import 'package:go_router/go_router.dart';
  5. import 'package:tdesign_flutter/tdesign_flutter.dart';
  6. import '../../core/i18n/app_localizations.dart';
  7. import '../../core/theme/app_colors.dart';
  8. import 'nav_bar_config.dart';
  9. class AppShell extends ConsumerWidget {
  10. final Widget child;
  11. const AppShell({super.key, required this.child});
  12. bool _showBottomBar(String location) {
  13. return location == '/' || location == '/messages' || location == '/profile';
  14. }
  15. int _tabIndex(String location) {
  16. if (location.startsWith('/messages')) return 0;
  17. if (location == '/') return 1;
  18. if (location.startsWith('/profile')) return 2;
  19. return 1;
  20. }
  21. /// 根页面 NavBar 配置直接从路由推导,避免 Timer 延迟更新的竞态问题
  22. NavBarConfig _rootConfig(String location, AppLocalizations l10n) {
  23. if (location.startsWith('/messages')) {
  24. return NavBarConfig(
  25. title: l10n.get('tabMessages'),
  26. showBack: true,
  27. leadingIcon: Icons.close,
  28. );
  29. }
  30. if (location == '/') {
  31. return NavBarConfig(
  32. title: l10n.get('appName'),
  33. showBack: true,
  34. leadingIcon: Icons.close,
  35. );
  36. }
  37. if (location.startsWith('/profile')) {
  38. return NavBarConfig(
  39. title: l10n.get('tabProfile'),
  40. showBack: true,
  41. leadingIcon: Icons.close,
  42. );
  43. }
  44. return NavBarConfig.home;
  45. }
  46. @override
  47. Widget build(BuildContext context, WidgetRef ref) {
  48. final l10n = AppLocalizations.of(context);
  49. final location = GoRouterState.of(context).uri.toString();
  50. final showBottomBar = _showBottomBar(location);
  51. // 根页面从 location 同步推导,子页面走 provider(由页面 build 时更新)
  52. final config = showBottomBar
  53. ? _rootConfig(location, l10n)
  54. : ref.watch(navBarConfigProvider);
  55. SystemChrome.setSystemUIOverlayStyle(
  56. const SystemUiOverlayStyle(
  57. statusBarColor: AppColors.bgCard,
  58. statusBarIconBrightness: Brightness.dark,
  59. statusBarBrightness: Brightness.light,
  60. ),
  61. );
  62. return Scaffold(
  63. backgroundColor: AppColors.bgPage,
  64. body: Column(
  65. crossAxisAlignment: CrossAxisAlignment.stretch,
  66. children: [
  67. _buildNavBar(config, location, context),
  68. Expanded(child: child),
  69. if (showBottomBar) _buildBottomBar(location, context, l10n),
  70. ],
  71. ),
  72. );
  73. }
  74. Widget _buildNavBar(
  75. NavBarConfig config,
  76. String location,
  77. BuildContext context,
  78. ) {
  79. final isRoot = _showBottomBar(location);
  80. List<TDNavBarItem>? leftItems;
  81. if (config.showBack) {
  82. final icon = config.leadingIcon ?? TDIcons.chevron_left;
  83. leftItems = [
  84. TDNavBarItem(
  85. icon: icon,
  86. iconSize: 22,
  87. iconColor: AppColors.textPrimary,
  88. action:
  89. config.onBack ??
  90. (isRoot
  91. ? () => SystemNavigator.pop()
  92. : () => GoRouter.of(context).pop()),
  93. ),
  94. ];
  95. }
  96. List<TDNavBarItem>? rightItems;
  97. if (config.showRight && config.rightWidget != null) {
  98. rightItems = [TDNavBarItem(iconWidget: config.rightWidget, iconSize: 22)];
  99. }
  100. return TDNavBar(
  101. title: config.title,
  102. titleColor: AppColors.textPrimary,
  103. titleFontWeight: FontWeight.w600,
  104. titleFont: Font(size: AppFontSizes.title.toInt(), lineHeight: 26),
  105. backgroundColor: AppColors.bgCard,
  106. height: 56,
  107. centerTitle: true,
  108. useDefaultBack: false,
  109. screenAdaptation: true,
  110. leftBarItems: leftItems,
  111. rightBarItems: rightItems,
  112. );
  113. }
  114. Widget _buildBottomBar(
  115. String location,
  116. BuildContext context,
  117. AppLocalizations l10n,
  118. ) {
  119. return LayoutBuilder(
  120. builder: (ctx, constraints) {
  121. if (constraints.maxWidth <= 0) {
  122. return const SizedBox.shrink();
  123. }
  124. final bottomPadding = MediaQuery.of(ctx).padding.bottom;
  125. return Padding(
  126. padding: EdgeInsets.only(top: 8, bottom: 8 + bottomPadding),
  127. child: TDBottomTabBar(
  128. TDBottomTabBarBasicType.iconText,
  129. useSafeArea: false,
  130. componentType: TDBottomTabBarComponentType.label,
  131. outlineType: TDBottomTabBarOutlineType.capsule,
  132. currentIndex: _tabIndex(location),
  133. navigationTabs: [
  134. TDBottomTabBarTabConfig(
  135. tabText: l10n.get('tabMessages'),
  136. selectedIcon: const Icon(
  137. Icons.notifications,
  138. size: 22,
  139. color: AppColors.primary,
  140. ),
  141. unselectedIcon: const Icon(
  142. Icons.notifications_outlined,
  143. size: 22,
  144. color: AppColors.textSecondary,
  145. ),
  146. selectTabTextStyle: const TextStyle(
  147. fontSize: 10,
  148. fontWeight: FontWeight.w600,
  149. color: AppColors.primary,
  150. ),
  151. unselectTabTextStyle: const TextStyle(
  152. fontSize: 10,
  153. color: AppColors.textSecondary,
  154. ),
  155. onTap: () => context.go('/messages'),
  156. ),
  157. TDBottomTabBarTabConfig(
  158. tabText: l10n.get('tabWorkbench'),
  159. selectedIcon: const Icon(
  160. Icons.dashboard,
  161. size: 22,
  162. color: AppColors.primary,
  163. ),
  164. unselectedIcon: const Icon(
  165. Icons.dashboard_outlined,
  166. size: 22,
  167. color: AppColors.textSecondary,
  168. ),
  169. selectTabTextStyle: const TextStyle(
  170. fontSize: 10,
  171. fontWeight: FontWeight.w600,
  172. color: AppColors.primary,
  173. ),
  174. unselectTabTextStyle: const TextStyle(
  175. fontSize: 10,
  176. color: AppColors.textSecondary,
  177. ),
  178. onTap: () => context.go('/'),
  179. ),
  180. TDBottomTabBarTabConfig(
  181. tabText: l10n.get('tabProfile'),
  182. selectedIcon: const Icon(
  183. Icons.person,
  184. size: 22,
  185. color: AppColors.primary,
  186. ),
  187. unselectedIcon: const Icon(
  188. Icons.person_outline,
  189. size: 22,
  190. color: AppColors.textSecondary,
  191. ),
  192. selectTabTextStyle: const TextStyle(
  193. fontSize: 10,
  194. fontWeight: FontWeight.w600,
  195. color: AppColors.primary,
  196. ),
  197. unselectTabTextStyle: const TextStyle(
  198. fontSize: 10,
  199. color: AppColors.textSecondary,
  200. ),
  201. onTap: () => context.go('/profile'),
  202. ),
  203. ],
  204. ),
  205. );
  206. },
  207. );
  208. }
  209. }