app_scaffold.dart 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. import 'package:flutter_riverpod/flutter_riverpod.dart';
  4. import '../../core/theme/app_colors.dart';
  5. import 'package:go_router/go_router.dart';
  6. import 'package:tdesign_flutter/tdesign_flutter.dart';
  7. import '../../core/i18n/app_localizations.dart';
  8. import '../../core/theme/app_colors_extension.dart';
  9. import 'nav_bar_config.dart';
  10. /// 应用级 Scaffold:NavBar + body + 可选 BottomTabBar
  11. ///
  12. /// 替代原来的 AppShell。每个页面自行包裹,无需 ShellRoute。
  13. class AppScaffold extends ConsumerWidget {
  14. final Widget body;
  15. final bool showTabBar;
  16. const AppScaffold({super.key, required this.body, this.showTabBar = false});
  17. bool _isRootTab(String location) {
  18. return location == '/' || location == '/messages' || location == '/profile';
  19. }
  20. NavBarConfig _rootConfig(String location, AppLocalizations l10n) {
  21. if (location.startsWith('/messages')) {
  22. return NavBarConfig(
  23. title: l10n.get('tabMessages'),
  24. showBack: true,
  25. leadingIcon: Icons.close,
  26. );
  27. }
  28. if (location == '/') {
  29. return NavBarConfig(
  30. title: l10n.get('appName'),
  31. showBack: true,
  32. leadingIcon: Icons.close,
  33. );
  34. }
  35. if (location.startsWith('/profile')) {
  36. return NavBarConfig(
  37. title: l10n.get('tabProfile'),
  38. showBack: true,
  39. leadingIcon: Icons.close,
  40. );
  41. }
  42. return NavBarConfig.home;
  43. }
  44. @override
  45. Widget build(BuildContext context, WidgetRef ref) {
  46. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  47. final l10n = AppLocalizations.of(context);
  48. final location = GoRouterState.of(context).uri.toString();
  49. final config = _isRootTab(location)
  50. ? _rootConfig(location, l10n)
  51. : ref.watch(navBarConfigProvider);
  52. SystemChrome.setSystemUIOverlayStyle(
  53. SystemUiOverlayStyle(
  54. statusBarColor: colors.bgCard,
  55. statusBarIconBrightness: Brightness.dark,
  56. statusBarBrightness: Brightness.light,
  57. ),
  58. );
  59. return Scaffold(
  60. backgroundColor: colors.bgPage,
  61. body: Column(
  62. crossAxisAlignment: CrossAxisAlignment.stretch,
  63. children: [
  64. _NavBarView(
  65. config: config,
  66. location: location,
  67. onBack: () {
  68. if (_isRootTab(location)) {
  69. SystemNavigator.pop();
  70. } else {
  71. GoRouter.of(context).pop();
  72. }
  73. },
  74. ),
  75. Expanded(child: body),
  76. if (showTabBar)
  77. Container(
  78. color: colors.bgCard,
  79. child: _AppTabBar(location: location),
  80. ),
  81. ],
  82. ),
  83. );
  84. }
  85. }
  86. class _NavBarView extends StatelessWidget {
  87. final NavBarConfig config;
  88. final String location;
  89. final VoidCallback onBack;
  90. const _NavBarView({
  91. required this.config,
  92. required this.location,
  93. required this.onBack,
  94. });
  95. @override
  96. Widget build(BuildContext context) {
  97. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  98. List<TDNavBarItem>? leftItems;
  99. if (config.showBack) {
  100. final icon = config.leadingIcon ?? TDIcons.chevron_left;
  101. leftItems = [
  102. TDNavBarItem(
  103. icon: icon,
  104. iconSize: 22,
  105. iconColor: colors.textPrimary,
  106. action: config.onBack ?? onBack,
  107. ),
  108. ];
  109. }
  110. List<TDNavBarItem>? rightItems;
  111. if (config.showRight && config.rightWidget != null) {
  112. rightItems = [TDNavBarItem(iconWidget: config.rightWidget, iconSize: 22)];
  113. }
  114. return TDNavBar(
  115. title: config.title,
  116. titleColor: colors.textPrimary,
  117. titleFontWeight: FontWeight.w600,
  118. titleFont: Font(size: AppFontSizes.title.toInt(), lineHeight: 26),
  119. backgroundColor: colors.bgCard,
  120. height: 56,
  121. centerTitle: true,
  122. useDefaultBack: false,
  123. screenAdaptation: true,
  124. leftBarItems: leftItems,
  125. rightBarItems: rightItems,
  126. );
  127. }
  128. }
  129. class _AppTabBar extends StatelessWidget {
  130. final String location;
  131. const _AppTabBar({required this.location});
  132. static int tabIndex(String location) {
  133. if (location.startsWith('/messages')) return 0;
  134. if (location == '/' ||
  135. (!location.startsWith('/messages') &&
  136. !location.startsWith('/profile'))) {
  137. return 1;
  138. }
  139. return 2;
  140. }
  141. @override
  142. Widget build(BuildContext context) {
  143. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  144. final l10n = AppLocalizations.of(context);
  145. return LayoutBuilder(
  146. builder: (ctx, constraints) {
  147. if (constraints.maxWidth <= 0) return const SizedBox.shrink();
  148. final bottomPadding = MediaQuery.of(ctx).padding.bottom;
  149. return Padding(
  150. padding: EdgeInsets.only(top: 0, bottom: 8 + bottomPadding),
  151. child: TDBottomTabBar(
  152. TDBottomTabBarBasicType.iconText,
  153. useSafeArea: false,
  154. componentType: TDBottomTabBarComponentType.label,
  155. outlineType: TDBottomTabBarOutlineType.filled,
  156. backgroundColor: colors.bgCard,
  157. dividerColor: Colors.transparent,
  158. selectedBgColor: colors.primaryLight,
  159. unselectedBgColor: Colors.transparent,
  160. currentIndex: tabIndex(location),
  161. navigationTabs: [
  162. TDBottomTabBarTabConfig(
  163. tabText: l10n.get('tabMessages'),
  164. selectedIcon: Icon(
  165. Icons.notifications,
  166. size: 22,
  167. color: colors.primary,
  168. ),
  169. unselectedIcon: Icon(
  170. Icons.notifications_outlined,
  171. size: 22,
  172. color: colors.textSecondary,
  173. ),
  174. selectTabTextStyle: TextStyle(
  175. fontSize: 10,
  176. fontWeight: FontWeight.w600,
  177. color: colors.primary,
  178. ),
  179. unselectTabTextStyle: TextStyle(
  180. fontSize: 10,
  181. color: colors.textSecondary,
  182. ),
  183. onTap: () => context.go('/messages'),
  184. ),
  185. TDBottomTabBarTabConfig(
  186. tabText: l10n.get('tabWorkbench'),
  187. selectedIcon: Icon(
  188. Icons.dashboard,
  189. size: 22,
  190. color: colors.primary,
  191. ),
  192. unselectedIcon: Icon(
  193. Icons.dashboard_outlined,
  194. size: 22,
  195. color: colors.textSecondary,
  196. ),
  197. selectTabTextStyle: TextStyle(
  198. fontSize: 10,
  199. fontWeight: FontWeight.w600,
  200. color: colors.primary,
  201. ),
  202. unselectTabTextStyle: TextStyle(
  203. fontSize: 10,
  204. color: colors.textSecondary,
  205. ),
  206. onTap: () => context.go('/'),
  207. ),
  208. TDBottomTabBarTabConfig(
  209. tabText: l10n.get('tabProfile'),
  210. selectedIcon: Icon(
  211. Icons.person,
  212. size: 22,
  213. color: colors.primary,
  214. ),
  215. unselectedIcon: Icon(
  216. Icons.person_outline,
  217. size: 22,
  218. color: colors.textSecondary,
  219. ),
  220. selectTabTextStyle: TextStyle(
  221. fontSize: 10,
  222. fontWeight: FontWeight.w600,
  223. color: colors.primary,
  224. ),
  225. unselectTabTextStyle: TextStyle(
  226. fontSize: 10,
  227. color: colors.textSecondary,
  228. ),
  229. onTap: () => context.go('/profile'),
  230. ),
  231. ],
  232. ),
  233. );
  234. },
  235. );
  236. }
  237. }