From 2dd4627bdcbaf20762388337379ffb5f96943621 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Sat, 17 Oct 2026 22:44:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(flutter):=20=E7=B2=BE=E7=AE=80=E9=A6=96?= =?UTF-8?q?=E9=A1=B5=E6=A0=87=E9=A2=98=E5=B9=B6=E9=87=8D=E5=81=9A=E7=A9=BA?= =?UTF-8?q?=E6=80=81=E8=A7=86=E8=A7=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/widgets/empty_state.dart | 71 +++++++++++++------- lib/pages/discover/discover_view.dart | 21 +----- test/discover/home_all_dramas_grid_test.dart | 19 ++++-- test/empty_state/empty_state_test.dart | 51 ++++++++++++-- test/widget_test.dart | 28 +++++--- 5 files changed, 127 insertions(+), 63 deletions(-) diff --git a/lib/core/widgets/empty_state.dart b/lib/core/widgets/empty_state.dart index b9b6093..3aac4f7 100644 --- a/lib/core/widgets/empty_state.dart +++ b/lib/core/widgets/empty_state.dart @@ -4,15 +4,17 @@ import 'package:flutter/material.dart'; import '../theme/app_colors.dart'; -const double _regularArtSize = 96; -const double _compactArtSize = 64; -const double _regularIconSize = 42; -const double _compactIconSize = 28; -const double _regularRadius = 28; -const double _compactRadius = 18; -const double _maxContentWidth = 280; +const double _regularArtSize = 84; +const double _compactArtSize = 60; +const double _regularInnerSize = 56; +const double _compactInnerSize = 40; +const double _regularIconSize = 27; +const double _compactIconSize = 21; +const double _maxContentWidth = 260; const double _actionMinHeight = 44; -const double _artOpacity = 0.11; +const double _outerArtOpacity = 0.055; +const double _outerBorderOpacity = 0.11; +const double _innerArtOpacity = 0.11; /// 统一空列表占位:柔和图标、标题、可选说明与操作。 class EmptyState extends StatelessWidget { @@ -39,6 +41,7 @@ class EmptyState extends StatelessWidget { final dark = Theme.of(context).brightness == Brightness.dark; final iconColor = dark ? AppColors.blueSoft : AppColors.blue; final artSize = compact ? _compactArtSize : _regularArtSize; + final innerSize = compact ? _compactInnerSize : _regularInnerSize; final iconSize = compact ? _compactIconSize : _regularIconSize; final hasAction = actionLabel != null && onAction != null; return Center( @@ -46,7 +49,11 @@ class EmptyState extends StatelessWidget { key: const Key('empty-state-content'), constraints: const BoxConstraints(maxWidth: _maxContentWidth), child: Padding( - padding: EdgeInsets.all(compact ? 16 : 24), + key: const Key('empty-state-padding'), + padding: EdgeInsets.symmetric( + horizontal: compact ? 16 : 24, + vertical: compact ? 16 : 28, + ), child: Semantics( container: true, label: title, @@ -60,20 +67,35 @@ class EmptyState extends StatelessWidget { height: artSize, alignment: Alignment.center, decoration: BoxDecoration( - color: AppColors.blue.withValues(alpha: _artOpacity), - borderRadius: BorderRadius.circular( - compact ? _compactRadius : _regularRadius, + shape: BoxShape.circle, + color: AppColors.blue.withValues(alpha: _outerArtOpacity), + border: Border.all( + color: AppColors.blue.withValues( + alpha: _outerBorderOpacity, + ), ), ), - child: Icon( - icon, - key: const Key('empty-state-icon'), - size: iconSize, - color: iconColor, + child: Container( + key: const Key('empty-state-art-inner'), + width: innerSize, + height: innerSize, + alignment: Alignment.center, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColors.blue.withValues( + alpha: _innerArtOpacity, + ), + ), + child: Icon( + icon, + key: const Key('empty-state-icon'), + size: iconSize, + color: iconColor, + ), ), ), ), - SizedBox(height: compact ? 10 : 16), + SizedBox(height: compact ? 10 : 14), ExcludeSemantics( child: Text( title, @@ -83,7 +105,7 @@ class EmptyState extends StatelessWidget { textAlign: TextAlign.center, style: TextStyle( color: scheme.onSurface, - fontSize: compact ? 14 : 16, + fontSize: 15, fontWeight: FontWeight.w600, height: 1.35, ), @@ -99,14 +121,14 @@ class EmptyState extends StatelessWidget { textAlign: TextAlign.center, style: const TextStyle( color: AppColors.muted, - fontSize: 13, - height: 1.4, + fontSize: 12.5, + height: 1.45, ), ), ), ], if (hasAction) ...[ - const SizedBox(height: 10), + const SizedBox(height: 8), TextButton( key: const Key('empty-state-action'), onPressed: onAction, @@ -115,7 +137,10 @@ class EmptyState extends StatelessWidget { minimumSize: const Size(44, _actionMinHeight), padding: const EdgeInsets.symmetric(horizontal: 14), ), - child: Text(actionLabel!), + child: Text( + actionLabel!, + style: const TextStyle(fontSize: 13), + ), ), ], ], diff --git a/lib/pages/discover/discover_view.dart b/lib/pages/discover/discover_view.dart index 0dc2d53..fc8ea7f 100644 --- a/lib/pages/discover/discover_view.dart +++ b/lib/pages/discover/discover_view.dart @@ -144,24 +144,8 @@ class _DiscoverPageState extends State { ), SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.fromLTRB(16, 18, 16, 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _categories(l10n, categories), - const SizedBox(height: 18), - Text( - l10n.allDramas, - key: const Key('home-all-dramas-title'), - style: const TextStyle( - color: AppColors.onPrimary, - fontSize: 18, - fontWeight: FontWeight.w700, - height: 1.2, - ), - ), - ], - ), + padding: const EdgeInsets.fromLTRB(16, 18, 16, 14), + child: _categories(l10n, categories), ), ), _HomeDramaGrid( @@ -277,6 +261,7 @@ class _DiscoverPageState extends State { (code: 'category-${category.id}', label: category.name), ]; return SizedBox( + key: const Key('home-category-chips'), height: 32, child: ListView.separated( scrollDirection: Axis.horizontal, diff --git a/test/discover/home_all_dramas_grid_test.dart b/test/discover/home_all_dramas_grid_test.dart index 90652be..b6d577f 100644 --- a/test/discover/home_all_dramas_grid_test.dart +++ b/test/discover/home_all_dramas_grid_test.dart @@ -37,13 +37,24 @@ void main() { await tester.pumpAndSettle(); expect(find.text('Continue Watching'), findsNothing); - expect(find.text('All Dramas'), findsOneWidget); + expect(find.byKey(const Key('home-all-dramas-title')), findsNothing); expect(find.text('All'), findsOneWidget); expect(find.text('For You'), findsNothing); expect( tester.getCenter(find.text('All')).dx, lessThan(tester.getCenter(find.text('Hot')).dx), ); + expect( + tester + .getTopLeft( + find.byKey(const Key('home-drama-card-the-signal-demo')), + ) + .dy - + tester + .getBottomLeft(find.byKey(const Key('home-category-chips'))) + .dy, + closeTo(14, 0.1), + ); final grid = tester.widget( find.byKey(const Key('home-all-dramas-grid')), @@ -135,7 +146,7 @@ void main() { await tester.tap(find.byKey(const Key('tab-discover'))); await tester.pumpAndSettle(); - expect(find.text('All Dramas'), findsOneWidget); + expect(find.byKey(const Key('home-all-dramas-title')), findsNothing); expect(find.text('No matching dramas.'), findsOneWidget); await tester.tap(find.text('Romance')); @@ -154,7 +165,7 @@ void main() { await tester.tap(find.byKey(const Key('tab-discover'))); await tester.pumpAndSettle(); - expect(find.text('All Dramas'), findsOneWidget); + expect(find.byKey(const Key('home-all-dramas-title')), findsNothing); expect(find.text('Categories could not be loaded.'), findsOneWidget); expect(find.text('Retry'), findsOneWidget); expect(find.byKey(const Key('home-all-dramas-grid')), findsNothing); @@ -176,7 +187,7 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.text('All Dramas'), findsOneWidget); + expect(find.byKey(const Key('home-all-dramas-title')), findsNothing); expect(find.text('City Lights & Velvet Nights'), findsOneWidget); expect(tester.takeException(), isNull); }); diff --git a/test/empty_state/empty_state_test.dart b/test/empty_state/empty_state_test.dart index aa6cdec..6f9b730 100644 --- a/test/empty_state/empty_state_test.dart +++ b/test/empty_state/empty_state_test.dart @@ -23,24 +23,51 @@ void main() { ); expect(find.byKey(const Key('empty-state-art')), findsOneWidget); + expect(find.byKey(const Key('empty-state-art-inner')), findsOneWidget); expect(find.byKey(const Key('empty-state-icon')), findsOneWidget); expect(find.text('No stories'), findsOneWidget); expect(find.text('New content will appear here.'), findsOneWidget); expect( tester.getSize(find.byKey(const Key('empty-state-art'))), - const Size(96, 96), + const Size(84, 84), + ); + expect( + tester.getSize(find.byKey(const Key('empty-state-art-inner'))), + const Size(56, 56), ); expect( tester.getSize(find.byKey(const Key('empty-state-icon'))), - const Size(42, 42), + const Size(27, 27), + ); + final outer = tester.widget( + find.byKey(const Key('empty-state-art')), + ); + final outerDecoration = outer.decoration! as BoxDecoration; + expect(outerDecoration.shape, BoxShape.circle); + expect(outerDecoration.color?.a, closeTo(0.055, 0.001)); + expect(outerDecoration.border!.top.color.a, closeTo(0.11, 0.001)); + final inner = tester.widget( + find.byKey(const Key('empty-state-art-inner')), + ); + expect((inner.decoration! as BoxDecoration).color?.a, closeTo(0.11, 0.001)); + expect( + tester + .widget(find.byKey(const Key('empty-state-padding'))) + .padding, + const EdgeInsets.symmetric(horizontal: 24, vertical: 28), ); expect( tester .widget(find.byKey(const Key('empty-state-title'))) .style ?.fontSize, - 16, + 15, ); + final subtitle = tester.widget( + find.text('New content will appear here.'), + ); + expect(subtitle.style?.fontSize, 12.5); + expect(tester.widget(find.text('Retry')).style?.fontSize, 13); expect( tester.getSize(find.byKey(const Key('empty-state-action'))).height, greaterThanOrEqualTo(44), @@ -72,18 +99,28 @@ void main() { expect( tester.getSize(find.byKey(const Key('empty-state-art'))), - const Size(64, 64), + const Size(60, 60), + ); + expect( + tester.getSize(find.byKey(const Key('empty-state-art-inner'))), + const Size(40, 40), ); expect( tester.getSize(find.byKey(const Key('empty-state-icon'))), - const Size(28, 28), + const Size(21, 21), ); expect( tester .widget(find.byKey(const Key('empty-state-title'))) .style ?.fontSize, - 14, + 15, + ); + expect( + tester + .widget(find.byKey(const Key('empty-state-padding'))) + .padding, + const EdgeInsets.symmetric(horizontal: 16, vertical: 16), ); expect(find.byKey(const Key('empty-state-action')), findsNothing); }); @@ -129,7 +166,7 @@ void main() { final content = tester.widget( find.byKey(const Key('empty-state-content')), ); - expect(content.constraints.maxWidth, 280); + expect(content.constraints.maxWidth, 260); expect(tester.takeException(), isNull); }); } diff --git a/test/widget_test.dart b/test/widget_test.dart index 93d3eaf..3efc430 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -155,7 +155,7 @@ void main() { await tester.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); - expect(find.text('All Dramas'), findsOneWidget); + expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget); await tester.binding.handlePopRoute(); await tester.pumpAndSettle(); expect( @@ -186,15 +186,17 @@ void main() { .active, isFalse, ); - expect(find.text('All Dramas'), findsOneWidget); + expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget); await tester.binding.handlePopRoute(); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('drawer-discover'))); await tester.pumpAndSettle(); - expect(find.text('All Dramas'), findsOneWidget); + expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget); expect( - Navigator.of(tester.element(find.text('All Dramas'))).canPop(), + Navigator.of( + tester.element(find.byKey(const Key('home-all-dramas-grid'))), + ).canPop(), isFalse, ); }, @@ -313,7 +315,7 @@ void main() { await tester.pumpAndSettle(); expect(find.text('All'), findsOneWidget); - expect(find.text('All Dramas'), findsOneWidget); + expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget); expect(find.text('Rewards'), findsOneWidget); expect(find.text(r'$ 0'), findsOneWidget); expect(find.text('Urban Romance', skipOffstage: false), findsOneWidget); @@ -345,10 +347,12 @@ void main() { await tester.pumpAndSettle(); await tester.tap(find.text('Home')); await tester.pumpAndSettle(); - expect(find.text('All Dramas'), findsOneWidget); + expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget); expect(find.byTooltip('Search'), findsNothing); expect( - Navigator.of(tester.element(find.text('All Dramas'))).canPop(), + Navigator.of( + tester.element(find.byKey(const Key('home-all-dramas-grid'))), + ).canPop(), isFalse, ); @@ -365,10 +369,12 @@ void main() { await tester.tap(find.byKey(const Key('tab-discover'))); await tester.pumpAndSettle(); - expect(find.text('All Dramas'), findsOneWidget); + expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget); expect(find.byTooltip('Search'), findsNothing); expect( - Navigator.of(tester.element(find.text('All Dramas'))).canPop(), + Navigator.of( + tester.element(find.byKey(const Key('home-all-dramas-grid'))), + ).canPop(), isFalse, ); await tester.tap(find.byKey(const Key('tab-my-list'))); @@ -564,7 +570,7 @@ void main() { await tester.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); - expect(find.text('All Dramas'), findsOneWidget); + expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget); await tester.binding.handlePopRoute(); await tester.pumpAndSettle(); expect(find.text('Quick access'), findsOneWidget); @@ -572,7 +578,7 @@ void main() { await tester.binding.handlePopRoute(); await tester.pumpAndSettle(); expect(find.text('Quick access'), findsNothing); - expect(find.text('All Dramas'), findsOneWidget); + expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget); expect( Navigator.of( tester.element(find.byKey(const Key('home-tabs-bar'))),