| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389 |
- 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<AppColorsExtension>()!;
- 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<AppColorsExtension>()!;
- List<TDNavBarItem>? 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<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'),
- ),
- ],
- ),
- );
- },
- );
- }
- }
|