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/edit')) { return l10n.get('editExpense'); } if (path.startsWith('/expense/create')) { 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/edit')) { return l10n.get('editApply'); } if (path.startsWith('/expense-apply/create')) { return l10n.get('expenseApplyRequest'); } if (path.startsWith('/overtime-apply/list')) { return l10n.get('overtimeList'); } if (path.startsWith('/overtime-apply/detail')) { return l10n.get('overtimeDetail'); } if (path.startsWith('/overtime-apply/create')) { return l10n.get('overtimeRequest'); } if (path.startsWith('/overtime-apply/edit')) { return l10n.get('editOvertime'); } if (path.startsWith('/vehicle-apply/list')) { return l10n.get('vehicleList'); } if (path.startsWith('/vehicle-apply/detail')) { return l10n.get('vehicleDetail'); } if (path.startsWith('/vehicle-apply/create')) { return l10n.get('vehicleRequest'); } if (path.startsWith('/vehicle-apply/edit')) { return l10n.get('editVehicle'); } 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-apply')) { return l10n.get('reportOvertimeDetail'); } if (path.startsWith('/report/vehicle-apply')) { 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'), ), ], ), ); }, ); } }