From 81787dc3aa149c083d305c37c71c59930d3dcedf Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Thu, 22 Oct 2026 16:38:14 +0800 Subject: [PATCH] =?UTF-8?q?style(nav):=20=E5=AF=B9=E9=BD=90=E5=9B=9B=20Tab?= =?UTF-8?q?=20=E5=BA=95=E6=A0=8F=E5=B8=83=E5=B1=80=E8=A7=84=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/theme/app_colors.dart | 5 +-- lib/core/theme/app_dimens.dart | 5 ++- lib/core/widgets/app_bottom_nav.dart | 19 ++-------- lib/core/widgets/app_root_tab.dart | 34 +++++++++++++++++ lib/pages/discover/discover_view.dart | 20 ++-------- lib/pages/home/home_view.dart | 32 +++------------- lib/pages/mine/mine_view.dart | 20 +++------- test/home/app_bottom_nav_test.dart | 53 ++++++++++++++++++++------- test/home/home_nav_ui_test.dart | 16 ++------ test/theme/app_dimens_test.dart | 5 ++- 10 files changed, 103 insertions(+), 106 deletions(-) diff --git a/lib/core/theme/app_colors.dart b/lib/core/theme/app_colors.dart index c9b764e..c8dcab2 100644 --- a/lib/core/theme/app_colors.dart +++ b/lib/core/theme/app_colors.dart @@ -69,10 +69,9 @@ abstract final class AppColors { /// 底部导航底色。 static const bar = ink; - /// 底部导航专用:纯黑背景、选中白、未选中灰与轻分割线。 + /// 底部导航专用:纯黑背景、选中白与未选中灰。 static const navBackground = Color(0xFF000000); static const navSelected = Color(0xFFFFFFFF); - static const navDivider = Color(0xFF1C1C1E); /// 卡片阴影(深色几乎不用阴影)。 static const cardShadow = Color(0x00000000); @@ -126,7 +125,7 @@ abstract final class AppColors { static const badgeBeige = raised; /// 导航未选中。 - static const navUnselected = Color(0xFF8A8A8E); + static const navUnselected = Color(0xFF8E8E93); /// 播放器次要文字。 static const playerMuted = Color(0xA3FFFFFF); diff --git a/lib/core/theme/app_dimens.dart b/lib/core/theme/app_dimens.dart index fba2ed4..299cb6f 100644 --- a/lib/core/theme/app_dimens.dart +++ b/lib/core/theme/app_dimens.dart @@ -37,6 +37,7 @@ abstract final class AppDimens { static const double previewSectionTitle = 14; static const double chipFont = 10; static const double navLabel = 11; + static const double navLabelGap = 4; static const double buttonHeight = 48; static const double primaryButtonHeight = 52; @@ -47,8 +48,8 @@ abstract final class AppDimens { static const double tapTarget = 44; static const double contentHeaderHeight = 46; - static const double navHeight = 68; - static const double navIcon = 24; + static const double navHeight = 56; + static const double navIcon = 26; static const double progressHeight = 3; static const double progressRadius = 2; static const double progressLineHeight = 3; diff --git a/lib/core/widgets/app_bottom_nav.dart b/lib/core/widgets/app_bottom_nav.dart index 6578ecc..a4163df 100644 --- a/lib/core/widgets/app_bottom_nav.dart +++ b/lib/core/widgets/app_bottom_nav.dart @@ -26,30 +26,17 @@ class AppBottomNavItem { /// 四槽底部导航:纯黑背景,白色选中态,灰色未选中态。 class AppBottomNav extends StatelessWidget { - const AppBottomNav({ - super.key, - required this.items, - this.backgroundColor = AppColors.navBackground, - this.barKey, - this.dark = false, - }); + const AppBottomNav({super.key, required this.items, this.barKey}); final List items; - final Color backgroundColor; final Key? barKey; - final bool dark; @override Widget build(BuildContext context) { final bar = Container( key: barKey, height: AppDimens.navHeight, - decoration: BoxDecoration( - color: dark ? AppColors.navBackground : backgroundColor, - border: const Border( - top: BorderSide(color: AppColors.navDivider, width: 0.5), - ), - ), + color: AppColors.navBackground, child: Row( children: [ for (final item in items) @@ -99,7 +86,7 @@ class _NavDestination extends StatelessWidget { : unselectedColor, ), ), - const SizedBox(height: 3), + const SizedBox(height: AppDimens.navLabelGap), Text( item.label, maxLines: 1, diff --git a/lib/core/widgets/app_root_tab.dart b/lib/core/widgets/app_root_tab.dart index 2a49059..798b2f7 100644 --- a/lib/core/widgets/app_root_tab.dart +++ b/lib/core/widgets/app_root_tab.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../../l10n/app_localizations.dart'; +import 'app_bottom_nav.dart'; /// 客户端根导航的固定顺序。 enum AppRootTab { home, recommend, following, mine } @@ -36,3 +37,36 @@ extension AppRootTabUi on AppRootTab { AppRootTab.mine => l10n.myTab, }; } + +/// 根导航唯一入口,确保四个主页面使用完全一致的底栏配置。 +class AppRootBottomNav extends StatelessWidget { + const AppRootBottomNav({ + super.key, + required this.selected, + required this.onSelected, + this.barKey, + }); + + final AppRootTab selected; + final ValueChanged onSelected; + final Key? barKey; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return AppBottomNav( + barKey: barKey, + items: [ + for (final tab in AppRootTab.values) + AppBottomNavItem( + tabKey: tab.tabKey, + icon: tab.icon, + selectedIcon: tab.selectedIcon, + label: tab.label(l10n), + selected: tab == selected, + onTap: () => onSelected(tab), + ), + ], + ); + } +} diff --git a/lib/pages/discover/discover_view.dart b/lib/pages/discover/discover_view.dart index 8ec05bd..f82d713 100644 --- a/lib/pages/discover/discover_view.dart +++ b/lib/pages/discover/discover_view.dart @@ -17,7 +17,6 @@ import '../../core/formatting/error_text.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_theme.dart'; -import '../../core/widgets/app_bottom_nav.dart'; import '../../core/widgets/app_pull_to_refresh.dart'; import '../../core/widgets/app_root_tab.dart'; import '../../core/widgets/catalog_debug_info.dart'; @@ -160,7 +159,7 @@ class _DiscoverPageState extends State { loading: logic.searchLoading, ), Expanded(child: _feed(l10n, categories)), - if (!embedded) _bottomNav(l10n), + if (!embedded) _bottomNav(), ], ), ), @@ -612,20 +611,9 @@ class _DiscoverPageState extends State { unawaited(playDramaFromCatalog(context, drama)); } - Widget _bottomNav(AppLocalizations l10n) => AppBottomNav( - items: [ - for (final tab in AppRootTab.values) - AppBottomNavItem( - tabKey: tab.tabKey, - icon: tab.icon, - selectedIcon: tab.selectedIcon, - label: tab.label(l10n), - selected: tab == AppRootTab.recommend, - onTap: tab == AppRootTab.recommend - ? () {} - : () => _act(_actionOf(tab)), - ), - ], + Widget _bottomNav() => AppRootBottomNav( + selected: AppRootTab.recommend, + onSelected: (tab) => _act(_actionOf(tab)), ); DiscoverPageAction _actionOf(AppRootTab tab) => switch (tab) { diff --git a/lib/pages/home/home_view.dart b/lib/pages/home/home_view.dart index 61b52ec..ec3eebe 100644 --- a/lib/pages/home/home_view.dart +++ b/lib/pages/home/home_view.dart @@ -16,7 +16,6 @@ import '../../core/data/services/category_controller.dart'; import '../../core/platform/haptics.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_theme.dart'; -import '../../core/widgets/app_bottom_nav.dart'; import '../../core/widgets/app_root_tab.dart'; import '../../core/widgets/catalog_debug_info.dart'; import '../../core/widgets/catalog_endpoint_editor.dart'; @@ -262,7 +261,6 @@ class _HomeTabsState extends State<_HomeTabs> { } Widget _scaffold(BuildContext context) { - final l10n = AppLocalizations.of(context)!; final feed = logic.tab == 0; final content = PopScope( canPop: false, @@ -275,7 +273,7 @@ class _HomeTabsState extends State<_HomeTabs> { backgroundColor: AppColors.transparent, resizeToAvoidBottomInset: false, body: _body(context), - bottomNavigationBar: logic.videoImmersive ? null : _nav(context, l10n), + bottomNavigationBar: logic.videoImmersive ? null : _nav(context), ), ); return AnnotatedRegion( @@ -451,31 +449,11 @@ class _HomeTabsState extends State<_HomeTabs> { ); } - Widget _nav(BuildContext context, AppLocalizations l10n) { - return AppBottomNav( + Widget _nav(BuildContext context) { + return AppRootBottomNav( barKey: const Key('home-tabs-bar'), - dark: logic.tab == 0, - items: [ - _navItem(context, l10n, AppRootTab.home), - _navItem(context, l10n, AppRootTab.recommend), - _navItem(context, l10n, AppRootTab.following), - _navItem(context, l10n, AppRootTab.mine), - ], - ); - } - - AppBottomNavItem _navItem( - BuildContext context, - AppLocalizations l10n, - AppRootTab tab, - ) { - return AppBottomNavItem( - tabKey: tab.tabKey, - icon: tab.icon, - selectedIcon: tab.selectedIcon, - label: tab.label(l10n), - selected: logic.tab == tab.index, - onTap: () => _selectTab(context, tab.index), + selected: AppRootTab.values[logic.tab], + onSelected: (tab) => _selectTab(context, tab.index), ); } diff --git a/lib/pages/mine/mine_view.dart b/lib/pages/mine/mine_view.dart index bba996f..7c83f61 100644 --- a/lib/pages/mine/mine_view.dart +++ b/lib/pages/mine/mine_view.dart @@ -12,7 +12,6 @@ import '../../core/data/services/wallet_controller.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_theme.dart'; -import '../../core/widgets/app_bottom_nav.dart'; import '../../core/widgets/app_root_tab.dart'; import '../../core/widgets/auth_gate.dart'; import '../../core/widgets/plain_page_scaffold.dart'; @@ -87,7 +86,7 @@ class _MyPageState extends State { children: [ _topBar(l10n), Expanded(child: _body(context, l10n)), - if (!embedded) _bottomNav(l10n), + if (!embedded) _bottomNav(), ], ), ); @@ -238,19 +237,10 @@ class _MyPageState extends State { _openProtected(RechargePage(onPurchased: () => unawaited(wallet.load()))); } - Widget _bottomNav(AppLocalizations l10n) { - return AppBottomNav( - items: [ - for (final tab in AppRootTab.values) - AppBottomNavItem( - tabKey: tab.tabKey, - icon: tab.icon, - selectedIcon: tab.selectedIcon, - label: tab.label(l10n), - selected: tab == AppRootTab.mine, - onTap: tab == AppRootTab.mine ? () {} : () => _act(_actionOf(tab)), - ), - ], + Widget _bottomNav() { + return AppRootBottomNav( + selected: AppRootTab.mine, + onSelected: (tab) => _act(_actionOf(tab)), ); } diff --git a/test/home/app_bottom_nav_test.dart b/test/home/app_bottom_nav_test.dart index ecf53c2..0b04e00 100644 --- a/test/home/app_bottom_nav_test.dart +++ b/test/home/app_bottom_nav_test.dart @@ -7,14 +7,22 @@ import 'package:fruitshorts/core/theme/app_dimens.dart'; import 'package:fruitshorts/core/widgets/app_bottom_nav.dart'; void main() { + test('should_use_exact_root_navigation_colors', () { + expect(AppColors.navBackground, const Color(0xFF000000)); + expect(AppColors.navSelected, const Color(0xFFFFFFFF)); + expect(AppColors.navUnselected, const Color(0xFF8E8E93)); + }); + testWidgets('should_render_four_slots_with_white_selected_state', ( tester, ) async { var theaterTaps = 0; + const barKey = Key('nav-bar'); await tester.pumpWidget( MaterialApp( home: Scaffold( bottomNavigationBar: AppBottomNav( + barKey: barKey, items: [ AppBottomNavItem( tabKey: const Key('tab-home'), @@ -60,6 +68,8 @@ void main() { final selectedLabel = tester.widget(find.text('Home')); expect(selectedLabel.style?.color, AppColors.navSelected); expect(selectedLabel.style?.fontWeight, FontWeight.w600); + expect(selectedLabel.style?.fontSize, AppDimens.navLabel); + expect(selectedLabel.style?.letterSpacing, 0); final unselectedLabel = tester.widget(find.text('Recommend')); expect(unselectedLabel.style?.color, AppColors.navUnselected); expect(unselectedLabel.style?.fontWeight, FontWeight.w400); @@ -70,20 +80,35 @@ void main() { Key('tab-mine'), ].map((key) => tester.getCenter(find.byKey(key)).dx).toList(); expect(centers, orderedEquals(centers.toList()..sort())); + final widths = [ + Key('tab-home'), + Key('tab-recommend'), + Key('tab-follow'), + Key('tab-mine'), + ].map((key) => tester.getSize(find.byKey(key)).width).toList(); + expect(widths.toSet(), hasLength(1)); expect( tester.getSize(find.byType(AppBottomNav)).height, AppDimens.navHeight, ); + final bar = tester.widget(find.byKey(barKey)); + expect(bar.color, AppColors.navBackground); + expect(bar.decoration, isNull); + final selectedIcon = tester.widget( + find.descendant( + of: find.byKey(const Key('tab-home')), + matching: find.byIcon(Icons.movie_rounded), + ), + ); + expect(selectedIcon.color, AppColors.navSelected); expect( - tester - .widget( - find.descendant( - of: find.byKey(const Key('tab-home')), - matching: find.byIcon(Icons.movie_rounded), - ), - ) - .color, - AppColors.navSelected, + tester.getSize(find.byIcon(Icons.movie_rounded)), + const Size(26, 26), + ); + expect( + tester.getTopLeft(find.text('Home')).dy - + tester.getBottomLeft(find.byIcon(Icons.movie_rounded)).dy, + AppDimens.navLabelGap, ); await tester.tap(find.byKey(const Key('tab-recommend'))); expect(theaterTaps, 1); @@ -99,7 +124,6 @@ void main() { data: const MediaQueryData(padding: EdgeInsets.only(bottom: 34)), child: Scaffold( bottomNavigationBar: AppBottomNav( - dark: true, barKey: barKey, items: [ AppBottomNavItem( @@ -144,12 +168,15 @@ void main() { ); expect(tester.getSize(find.byKey(barKey)).height, AppDimens.navHeight); - final coloredBox = tester.widget( + expect( find.descendant( of: find.byType(AppBottomNav), - matching: find.byType(ColoredBox), + matching: find.byWidgetPredicate( + (widget) => + widget is ColoredBox && widget.color == AppColors.navBackground, + ), ), + findsWidgets, ); - expect(coloredBox.color, AppColors.navBackground); }); } diff --git a/test/home/home_nav_ui_test.dart b/test/home/home_nav_ui_test.dart index 1e080fe..0e044f0 100644 --- a/test/home/home_nav_ui_test.dart +++ b/test/home/home_nav_ui_test.dart @@ -85,12 +85,8 @@ void main() { final barFinder = find.byKey(const Key('home-tabs-bar')); final bar = tester.widget(barFinder); - final decoration = bar.decoration as BoxDecoration; - final border = decoration.border as Border; - // 所有页面统一深色导航。 - expect(decoration.color, AppColors.navBackground); - expect(border.top.color, AppColors.navDivider); - expect(border.top.width, 0.5); + expect(bar.color, AppColors.navBackground); + expect(bar.decoration, isNull); expect( tester .widget( @@ -158,12 +154,8 @@ void main() { await tester.pumpAndSettle(); expect(haptics.selectionClicks, 1); final theaterBar = tester.widget(barFinder); - final theaterDecoration = theaterBar.decoration as BoxDecoration; - expect(theaterDecoration.color, AppColors.navBackground); - expect( - (theaterDecoration.border as Border).top.color, - AppColors.navDivider, - ); + expect(theaterBar.color, AppColors.navBackground); + expect(theaterBar.decoration, isNull); await tester.tap(find.byKey(const Key('tab-recommend'))); await tester.pumpAndSettle(); expect(haptics.selectionClicks, 1); diff --git a/test/theme/app_dimens_test.dart b/test/theme/app_dimens_test.dart index c7187bc..8a8cbfb 100644 --- a/test/theme/app_dimens_test.dart +++ b/test/theme/app_dimens_test.dart @@ -19,11 +19,12 @@ void main() { expect(AppDimens.sectionTitle, 16); expect(AppDimens.pageTitle, 20); expect(AppDimens.navLabel, 11); - expect(AppDimens.navIcon, 24); + expect(AppDimens.navLabelGap, 4); + expect(AppDimens.navIcon, 26); expect(AppDimens.buttonHeight, 48); expect(AppDimens.primaryButtonHeight, 52); expect(AppDimens.tapTarget, 44); - expect(AppDimens.navHeight, 68); + expect(AppDimens.navHeight, 56); expect(AppDimens.progressHeight, 3); }); }