import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/theme/app_colors.dart'; import 'package:go_router/go_router.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; import '../../core/i18n/app_localizations.dart'; import '../../core/theme/app_colors_extension.dart'; import 'nav_bar_config.dart'; /// 应用级 Scaffold:NavBar + body + 可选 BottomTabBar class AppScaffold extends ConsumerWidget { final Widget body; final bool showTabBar; final bool resizeToAvoidBottomInset; final NavBarConfig? navConfig; const AppScaffold({ super.key, required this.body, this.showTabBar = false, this.resizeToAvoidBottomInset = true, this.navConfig, }); bool _isRootTab(String location) { final path = location.split('?').first; return path == '/' || path == '/messages' || path == '/profile'; } String? _titleForRoute(String location, AppLocalizations l10n) { final path = location.split('?').first; if (path == '/') return l10n.get('appName'); if (path == '/messages') return l10n.get('tabMessages'); if (path == '/profile') return l10n.get('tabProfile'); if (path.startsWith('/expense/list')) return l10n.get('expenseList'); if (path.startsWith('/expense/import-apply')) return l10n.get('importExpenseApply'); if (path.startsWith('/expense/detail')) return l10n.get('expenseDetail'); if (path.startsWith('/expense/create') || path.startsWith('/expense/edit')) return l10n.get('expenseApply'); if (path.startsWith('/expense-apply/list')) return l10n.get('expenseApplyList'); if (path.startsWith('/expense-apply/detail')) return l10n.get('expenseApplyDetail'); if (path.startsWith('/expense-apply/create')) return l10n.get('expenseApplyRequest'); if (path.startsWith('/overtime/list')) return l10n.get('overtimeList'); if (path.startsWith('/overtime/detail')) return l10n.get('overtimeDetail'); if (path.startsWith('/overtime/create')) return l10n.get('overtimeRequest'); if (path.startsWith('/vehicle/list')) return l10n.get('vehicleList'); if (path.startsWith('/vehicle/detail')) return l10n.get('vehicleDetail'); if (path.startsWith('/vehicle/create')) return l10n.get('vehicleRequest'); if (path.startsWith('/outing-log/list')) return l10n.get('outingLogList'); if (path.startsWith('/outing-log/detail')) return l10n.get('outingLogDetail'); if (path.startsWith('/outing-log/create')) return l10n.get('outingLogCreate'); if (path.startsWith('/announcement/list')) return l10n.get('announcementList'); if (path.startsWith('/announcement/detail')) return l10n.get('announcementDetail'); if (path.startsWith('/announcement/create')) return l10n.get('announcementCreate'); if (path.startsWith('/report/expense-detail')) return l10n.get('reportExpenseDetail'); if (path.startsWith('/report/expense-apply-detail')) return l10n.get('reportExpenseApplyDetail'); if (path.startsWith('/report/overtime')) return l10n.get('reportOvertimeDetail'); if (path.startsWith('/report/vehicle')) return l10n.get('reportVehicleDetail'); if (path.startsWith('/report/outing-log')) return l10n.get('reportOutingLogDetail'); if (path.startsWith('/admin/permissions')) return l10n.get('permissionManagement'); return null; } NavBarConfig _rootConfig(String location, AppLocalizations l10n) { final path = location.split('?').first; if (path == '/messages') return NavBarConfig(title: l10n.get('tabMessages'), showBack: true, leadingIcon: Icons.close); if (path == '/') return NavBarConfig(title: l10n.get('appName'), showBack: true, leadingIcon: Icons.close); if (path == '/profile') return NavBarConfig(title: l10n.get('tabProfile'), showBack: true, leadingIcon: Icons.close); return NavBarConfig.home; } NavBarConfig _resolve(NavBarConfig cfg, String location, AppLocalizations l10n) { final routeTitle = _titleForRoute(location, l10n); if (routeTitle == null) return cfg; return NavBarConfig( title: routeTitle, showBack: cfg.showBack, showRight: cfg.showRight, rightWidget: cfg.rightWidget, onBack: cfg.onBack, leadingIcon: cfg.leadingIcon, ); } @override Widget build(BuildContext context, WidgetRef ref) { SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle(statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.dark), ); final colors = Theme.of(context).extension()!; final l10n = AppLocalizations.of(context); final location = GoRouterState.of(context).uri.toString(); final config = navConfig != null ? _resolve(navConfig!, location, l10n) : _rootConfig(location, l10n); // 页面可通过 pageBackProvider 覆盖返回按钮行为(如 create 页面的 _doPop) final pageBack = ref.watch(pageBackProvider); return Scaffold( resizeToAvoidBottomInset: resizeToAvoidBottomInset, backgroundColor: colors.bgPage, body: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _NavBarView( config: config, location: location, pageBack: pageBack, onBack: () { if (_isRootTab(location)) { SystemNavigator.pop(); } else { final router = GoRouter.of(context); if (router.canPop()) { router.pop(); } else { SystemNavigator.pop(); } } }, ), Expanded(child: body), if (showTabBar) Container(color: colors.bgCard, child: _AppTabBar(location: location)), ], ), ); } } class _NavBarView extends StatelessWidget { final NavBarConfig config; final String location; final VoidCallback? pageBack; final VoidCallback onBack; const _NavBarView({required this.config, required this.location, this.pageBack, required this.onBack}); @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; List? leftItems; if (config.showBack) { final icon = config.leadingIcon ?? TDIcons.chevron_left; // config.onBack(路由级声明)优先于 pageBack(页面级 Provider), // 防止 Provider 残留值泄漏到不相关的页面 final backAction = config.onBack ?? pageBack ?? onBack; leftItems = [TDNavBarItem(icon: icon, iconSize: 22, iconColor: colors.textPrimary, action: backAction)]; } List? rightItems; if (config.showRight && config.rightWidget != null) { rightItems = [TDNavBarItem(iconWidget: config.rightWidget, iconSize: 22)]; } return TDNavBar( title: config.title, titleColor: colors.textPrimary, titleFontWeight: FontWeight.w600, titleFont: Font(size: AppFontSizes.title.toInt(), lineHeight: 26), backgroundColor: colors.bgCard, height: 56, centerTitle: true, useDefaultBack: false, screenAdaptation: true, leftBarItems: leftItems, rightBarItems: rightItems, ); } } class _AppTabBar extends StatelessWidget { final String location; const _AppTabBar({required this.location}); static int tabIndex(String location) { if (location.startsWith('/messages')) return 0; if (location == '/' || (!location.startsWith('/messages') && !location.startsWith('/profile'))) return 1; return 2; } @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; final l10n = AppLocalizations.of(context); return LayoutBuilder(builder: (ctx, constraints) { if (constraints.maxWidth <= 0) return const SizedBox.shrink(); final bottomPadding = MediaQuery.of(ctx).padding.bottom; return Padding( padding: EdgeInsets.only(top: 0, bottom: 8 + bottomPadding), child: TDBottomTabBar( TDBottomTabBarBasicType.iconText, useSafeArea: false, componentType: TDBottomTabBarComponentType.label, outlineType: TDBottomTabBarOutlineType.filled, backgroundColor: colors.bgCard, dividerColor: Colors.transparent, selectedBgColor: colors.primaryLight, unselectedBgColor: Colors.transparent, currentIndex: tabIndex(location), navigationTabs: [ TDBottomTabBarTabConfig( tabText: l10n.get('tabMessages'), selectedIcon: Icon(Icons.notifications, size: 22, color: colors.primary), unselectedIcon: Icon(Icons.notifications_outlined, size: 22, color: colors.textSecondary), selectTabTextStyle: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: colors.primary), unselectTabTextStyle: TextStyle(fontSize: 10, color: colors.textSecondary), onTap: () => context.go('/messages'), ), TDBottomTabBarTabConfig( tabText: l10n.get('tabWorkbench'), selectedIcon: Icon(Icons.dashboard, size: 22, color: colors.primary), unselectedIcon: Icon(Icons.dashboard_outlined, size: 22, color: colors.textSecondary), selectTabTextStyle: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: colors.primary), unselectTabTextStyle: TextStyle(fontSize: 10, color: colors.textSecondary), onTap: () => context.go('/'), ), TDBottomTabBarTabConfig( tabText: l10n.get('tabProfile'), selectedIcon: Icon(Icons.person, size: 22, color: colors.primary), unselectedIcon: Icon(Icons.person_outline, size: 22, color: colors.textSecondary), selectTabTextStyle: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: colors.primary), unselectTabTextStyle: TextStyle(fontSize: 10, color: colors.textSecondary), onTap: () => context.go('/profile'), ), ], ), ); }); } }