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 /// /// 替代原来的 AppShell。每个页面自行包裹,无需 ShellRoute。 class AppScaffold extends ConsumerWidget { final Widget body; final bool showTabBar; final bool resizeToAvoidBottomInset; const AppScaffold({ super.key, required this.body, this.showTabBar = false, this.resizeToAvoidBottomInset = true, }); bool _isRootTab(String location) { return location == '/' || location == '/messages' || location == '/profile'; } NavBarConfig _pageConfig(String location, AppLocalizations l10n, WidgetRef ref) { // 从 provider 读取页面自定义属性(rightWidget 等),标题由路由决定 final custom = ref.watch(navBarConfigProvider); final title = _titleForRoute(location, l10n); if (title == null) return custom; return NavBarConfig( title: title, showBack: custom.showBack, showRight: custom.showRight, rightWidget: custom.rightWidget, leadingIcon: custom.leadingIcon, onBack: custom.onBack, ); } /// 路由 → 标题映射,新增路由只需在此添加一行 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/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-apply')) return l10n.get('expenseApplyReport'); if (path.startsWith('/report/expense')) return l10n.get('expenseReport'); if (path.startsWith('/report/overtime')) return l10n.get('overtimeReport'); if (path.startsWith('/report/vehicle')) return l10n.get('vehicleReport'); if (path.startsWith('/report/outing-log')) return l10n.get('outingLogReport'); // ── 管理 ── if (path.startsWith('/admin/permissions')) return l10n.get('permissionManagement'); return null; // 未知路由 → 回退到 provider } NavBarConfig _rootConfig(String location, AppLocalizations l10n) { if (location.startsWith('/messages')) { return NavBarConfig( title: l10n.get('tabMessages'), showBack: true, leadingIcon: Icons.close, ); } if (location == '/') { return NavBarConfig( title: l10n.get('appName'), showBack: true, leadingIcon: Icons.close, ); } if (location.startsWith('/profile')) { return NavBarConfig( title: l10n.get('tabProfile'), showBack: true, leadingIcon: Icons.close, ); } return NavBarConfig.home; } @override Widget build(BuildContext context, WidgetRef ref) { final colors = Theme.of(context).extension()!; final l10n = AppLocalizations.of(context); final location = GoRouterState.of(context).uri.toString(); final config = _isRootTab(location) ? _rootConfig(location, l10n) : _pageConfig(location, l10n, ref); return Scaffold( resizeToAvoidBottomInset: resizeToAvoidBottomInset, backgroundColor: colors.bgPage, body: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _NavBarView( config: config, location: location, onBack: () { if (_isRootTab(location)) { SystemNavigator.pop(); } else { GoRouter.of(context).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 onBack; const _NavBarView({ required this.config, required this.location, 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; leftItems = [ TDNavBarItem( icon: icon, iconSize: 22, iconColor: colors.textPrimary, action: config.onBack ?? onBack, ), ]; } 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'), ), ], ), ); }, ); } }