app_scaffold.dart 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  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 '../../features/shell/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(title: l10n.get('tabMessages'), showBack: true, leadingIcon: Icons.close);
  23. }
  24. if (location == '/') {
  25. return NavBarConfig(title: l10n.get('appName'), showBack: true, leadingIcon: Icons.close);
  26. }
  27. if (location.startsWith('/profile')) {
  28. return NavBarConfig(title: l10n.get('tabProfile'), showBack: true, leadingIcon: Icons.close);
  29. }
  30. return NavBarConfig.home;
  31. }
  32. @override
  33. Widget build(BuildContext context, WidgetRef ref) {
  34. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  35. final l10n = AppLocalizations.of(context);
  36. final location = GoRouterState.of(context).uri.toString();
  37. final config = _isRootTab(location)
  38. ? _rootConfig(location, l10n)
  39. : ref.watch(navBarConfigProvider);
  40. SystemChrome.setSystemUIOverlayStyle(
  41. SystemUiOverlayStyle(
  42. statusBarColor: colors.bgCard,
  43. statusBarIconBrightness: Brightness.dark,
  44. statusBarBrightness: Brightness.light,
  45. ),
  46. );
  47. return Scaffold(
  48. backgroundColor: colors.bgPage,
  49. body: Column(
  50. crossAxisAlignment: CrossAxisAlignment.stretch,
  51. children: [
  52. _NavBarView(config: config, location: location, onBack: () {
  53. if (_isRootTab(location)) {
  54. SystemNavigator.pop();
  55. } else {
  56. GoRouter.of(context).pop();
  57. }
  58. }),
  59. Expanded(child: body),
  60. if (showTabBar)
  61. Container(
  62. color: colors.bgCard,
  63. child: _AppTabBar(location: location),
  64. ),
  65. ],
  66. ),
  67. );
  68. }
  69. }
  70. class _NavBarView extends StatelessWidget {
  71. final NavBarConfig config;
  72. final String location;
  73. final VoidCallback onBack;
  74. const _NavBarView({required this.config, required this.location, required this.onBack});
  75. @override
  76. Widget build(BuildContext context) {
  77. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  78. List<TDNavBarItem>? leftItems;
  79. if (config.showBack) {
  80. final icon = config.leadingIcon ?? TDIcons.chevron_left;
  81. leftItems = [
  82. TDNavBarItem(
  83. icon: icon,
  84. iconSize: 22,
  85. iconColor: colors.textPrimary,
  86. action: config.onBack ?? onBack,
  87. ),
  88. ];
  89. }
  90. List<TDNavBarItem>? rightItems;
  91. if (config.showRight && config.rightWidget != null) {
  92. rightItems = [TDNavBarItem(iconWidget: config.rightWidget, iconSize: 22)];
  93. }
  94. return TDNavBar(
  95. title: config.title,
  96. titleColor: colors.textPrimary,
  97. titleFontWeight: FontWeight.w600,
  98. titleFont: Font(size: AppFontSizes.title.toInt(), lineHeight: 26),
  99. backgroundColor: colors.bgCard,
  100. height: 56,
  101. centerTitle: true,
  102. useDefaultBack: false,
  103. screenAdaptation: true,
  104. leftBarItems: leftItems,
  105. rightBarItems: rightItems,
  106. );
  107. }
  108. }
  109. class _AppTabBar extends StatelessWidget {
  110. final String location;
  111. const _AppTabBar({required this.location});
  112. static int tabIndex(String location) {
  113. if (location.startsWith('/messages')) return 0;
  114. if (location == '/' || (!location.startsWith('/messages') && !location.startsWith('/profile'))) return 1;
  115. return 2;
  116. }
  117. @override
  118. Widget build(BuildContext context) {
  119. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  120. final l10n = AppLocalizations.of(context);
  121. return LayoutBuilder(
  122. builder: (ctx, constraints) {
  123. if (constraints.maxWidth <= 0) return const SizedBox.shrink();
  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: Icon(Icons.notifications, size: 22, color: colors.primary),
  137. unselectedIcon: Icon(Icons.notifications_outlined, size: 22, color: colors.textSecondary),
  138. selectTabTextStyle: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: colors.primary),
  139. unselectTabTextStyle: TextStyle(fontSize: 10, color: colors.textSecondary),
  140. onTap: () => context.go('/messages'),
  141. ),
  142. TDBottomTabBarTabConfig(
  143. tabText: l10n.get('tabWorkbench'),
  144. selectedIcon: Icon(Icons.dashboard, size: 22, color: colors.primary),
  145. unselectedIcon: Icon(Icons.dashboard_outlined, size: 22, color: colors.textSecondary),
  146. selectTabTextStyle: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: colors.primary),
  147. unselectTabTextStyle: TextStyle(fontSize: 10, color: colors.textSecondary),
  148. onTap: () => context.go('/'),
  149. ),
  150. TDBottomTabBarTabConfig(
  151. tabText: l10n.get('tabProfile'),
  152. selectedIcon: Icon(Icons.person, size: 22, color: colors.primary),
  153. unselectedIcon: Icon(Icons.person_outline, size: 22, color: colors.textSecondary),
  154. selectTabTextStyle: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: colors.primary),
  155. unselectTabTextStyle: TextStyle(fontSize: 10, color: colors.textSecondary),
  156. onTap: () => context.go('/profile'),
  157. ),
  158. ],
  159. ),
  160. );
  161. },
  162. );
  163. }
  164. }