|
|
@@ -9,126 +9,92 @@ 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;
|
|
|
+ final NavBarConfig? navConfig;
|
|
|
|
|
|
const AppScaffold({
|
|
|
super.key,
|
|
|
required this.body,
|
|
|
this.showTabBar = false,
|
|
|
this.resizeToAvoidBottomInset = true,
|
|
|
+ this.navConfig,
|
|
|
});
|
|
|
|
|
|
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,
|
|
|
- );
|
|
|
+ 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/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('/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; // 未知路由 → 回退到 provider
|
|
|
+ return null;
|
|
|
}
|
|
|
|
|
|
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,
|
|
|
- );
|
|
|
- }
|
|
|
+ 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,
|
|
|
- ),
|
|
|
+ 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);
|
|
|
+ final config = navConfig != null
|
|
|
+ ? _resolve(navConfig!, location, l10n)
|
|
|
+ : _rootConfig(location, l10n);
|
|
|
+ // 页面可通过 pageBackProvider 覆盖返回按钮行为(如 create 页面的 _doPop)
|
|
|
+ final pageBack = ref.watch(pageBackProvider);
|
|
|
|
|
|
return Scaffold(
|
|
|
resizeToAvoidBottomInset: resizeToAvoidBottomInset,
|
|
|
@@ -139,20 +105,23 @@ class AppScaffold extends ConsumerWidget {
|
|
|
_NavBarView(
|
|
|
config: config,
|
|
|
location: location,
|
|
|
+ pageBack: pageBack,
|
|
|
onBack: () {
|
|
|
if (_isRootTab(location)) {
|
|
|
SystemNavigator.pop();
|
|
|
} else {
|
|
|
- GoRouter.of(context).pop();
|
|
|
+ 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),
|
|
|
- ),
|
|
|
+ Container(color: colors.bgCard, child: _AppTabBar(location: location)),
|
|
|
],
|
|
|
),
|
|
|
);
|
|
|
@@ -162,13 +131,9 @@ class AppScaffold extends ConsumerWidget {
|
|
|
class _NavBarView extends StatelessWidget {
|
|
|
final NavBarConfig config;
|
|
|
final String location;
|
|
|
+ final VoidCallback? pageBack;
|
|
|
final VoidCallback onBack;
|
|
|
-
|
|
|
- const _NavBarView({
|
|
|
- required this.config,
|
|
|
- required this.location,
|
|
|
- required this.onBack,
|
|
|
- });
|
|
|
+ const _NavBarView({required this.config, required this.location, this.pageBack, required this.onBack});
|
|
|
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
@@ -176,21 +141,15 @@ class _NavBarView extends StatelessWidget {
|
|
|
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,
|
|
|
- ),
|
|
|
- ];
|
|
|
+ // 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,
|
|
|
@@ -209,16 +168,11 @@ class _NavBarView extends StatelessWidget {
|
|
|
|
|
|
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;
|
|
|
- }
|
|
|
+ if (location == '/' || (!location.startsWith('/messages') && !location.startsWith('/profile'))) return 1;
|
|
|
return 2;
|
|
|
}
|
|
|
|
|
|
@@ -226,96 +180,46 @@ class _AppTabBar extends StatelessWidget {
|
|
|
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'),
|
|
|
- ),
|
|
|
- ],
|
|
|
- ),
|
|
|
- );
|
|
|
- },
|
|
|
- );
|
|
|
+ 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'),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ });
|
|
|
}
|
|
|
}
|