app_shell.dart 7.3 KB

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