| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323 |
- 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) {
- SystemChrome.setSystemUIOverlayStyle(
- const SystemUiOverlayStyle(
- statusBarColor: Colors.transparent,
- statusBarIconBrightness: Brightness.dark,
- ),
- );
- final colors = Theme.of(context).extension<AppColorsExtension>()!;
- 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<AppColorsExtension>()!;
- List<TDNavBarItem>? 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<TDNavBarItem>? 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<AppColorsExtension>()!;
- 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'),
- ),
- ],
- ),
- );
- },
- );
- }
- }
|