merge: 合并 K12 首页与空态视觉修订

This commit is contained in:
Zhou Weidong committed 2026-10-17 22:44:40 +08:00
commit b76228905c
5 files changed
+122 -58

No files matched your search

+43 -18
View File
@@ -4,15 +4,17 @@ import 'package:flutter/material.dart';
import '../theme/app_colors.dart'; import '../theme/app_colors.dart';
const double _regularArtSize = 96; const double _regularArtSize = 84;
const double _compactArtSize = 64; const double _compactArtSize = 60;
const double _regularIconSize = 42; const double _regularInnerSize = 56;
const double _compactIconSize = 28; const double _compactInnerSize = 40;
const double _regularRadius = 28; const double _regularIconSize = 27;
const double _compactRadius = 18; const double _compactIconSize = 21;
const double _maxContentWidth = 280; const double _maxContentWidth = 260;
const double _actionMinHeight = 44; 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 { class EmptyState extends StatelessWidget {
@@ -39,6 +41,7 @@ class EmptyState extends StatelessWidget {
final dark = Theme.of(context).brightness == Brightness.dark; final dark = Theme.of(context).brightness == Brightness.dark;
final iconColor = dark ? AppColors.blueSoft : AppColors.blue; final iconColor = dark ? AppColors.blueSoft : AppColors.blue;
final artSize = compact ? _compactArtSize : _regularArtSize; final artSize = compact ? _compactArtSize : _regularArtSize;
final innerSize = compact ? _compactInnerSize : _regularInnerSize;
final iconSize = compact ? _compactIconSize : _regularIconSize; final iconSize = compact ? _compactIconSize : _regularIconSize;
final hasAction = actionLabel != null && onAction != null; final hasAction = actionLabel != null && onAction != null;
return Center( return Center(
@@ -46,7 +49,11 @@ class EmptyState extends StatelessWidget {
key: const Key('empty-state-content'), key: const Key('empty-state-content'),
constraints: const BoxConstraints(maxWidth: _maxContentWidth), constraints: const BoxConstraints(maxWidth: _maxContentWidth),
child: Padding( 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( child: Semantics(
container: true, container: true,
label: title, label: title,
@@ -60,9 +67,23 @@ class EmptyState extends StatelessWidget {
height: artSize, height: artSize,
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.blue.withValues(alpha: _artOpacity), shape: BoxShape.circle,
borderRadius: BorderRadius.circular( color: AppColors.blue.withValues(alpha: _outerArtOpacity),
compact ? _compactRadius : _regularRadius, border: Border.all(
color: AppColors.blue.withValues(
alpha: _outerBorderOpacity,
),
),
),
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( child: Icon(
@@ -73,7 +94,8 @@ class EmptyState extends StatelessWidget {
), ),
), ),
), ),
SizedBox(height: compact ? 10 : 16), ),
SizedBox(height: compact ? 10 : 14),
ExcludeSemantics( ExcludeSemantics(
child: Text( child: Text(
title, title,
@@ -83,7 +105,7 @@ class EmptyState extends StatelessWidget {
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: scheme.onSurface, color: scheme.onSurface,
fontSize: compact ? 14 : 16, fontSize: 15,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
height: 1.35, height: 1.35,
), ),
@@ -99,14 +121,14 @@ class EmptyState extends StatelessWidget {
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
color: AppColors.muted, color: AppColors.muted,
fontSize: 13, fontSize: 12.5,
height: 1.4, height: 1.45,
), ),
), ),
), ),
], ],
if (hasAction) ...[ if (hasAction) ...[
const SizedBox(height: 10), const SizedBox(height: 8),
TextButton( TextButton(
key: const Key('empty-state-action'), key: const Key('empty-state-action'),
onPressed: onAction, onPressed: onAction,
@@ -115,7 +137,10 @@ class EmptyState extends StatelessWidget {
minimumSize: const Size(44, _actionMinHeight), minimumSize: const Size(44, _actionMinHeight),
padding: const EdgeInsets.symmetric(horizontal: 14), padding: const EdgeInsets.symmetric(horizontal: 14),
), ),
child: Text(actionLabel!), child: Text(
actionLabel!,
style: const TextStyle(fontSize: 13),
),
), ),
], ],
], ],
+3 -18
View File
@@ -144,24 +144,8 @@ class _DiscoverPageState extends State<DiscoverPage> {
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(16, 18, 16, 12), padding: const EdgeInsets.fromLTRB(16, 18, 16, 14),
child: Column( child: _categories(l10n, categories),
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,
),
),
],
),
), ),
), ),
_HomeDramaGrid( _HomeDramaGrid(
@@ -277,6 +261,7 @@ class _DiscoverPageState extends State<DiscoverPage> {
(code: 'category-${category.id}', label: category.name), (code: 'category-${category.id}', label: category.name),
]; ];
return SizedBox( return SizedBox(
key: const Key('home-category-chips'),
height: 32, height: 32,
child: ListView.separated( child: ListView.separated(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
+15 -4
View File
@@ -37,13 +37,24 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('Continue Watching'), findsNothing); 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('All'), findsOneWidget);
expect(find.text('For You'), findsNothing); expect(find.text('For You'), findsNothing);
expect( expect(
tester.getCenter(find.text('All')).dx, tester.getCenter(find.text('All')).dx,
lessThan(tester.getCenter(find.text('Hot')).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<SliverGrid>( final grid = tester.widget<SliverGrid>(
find.byKey(const Key('home-all-dramas-grid')), 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.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle(); 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); expect(find.text('No matching dramas.'), findsOneWidget);
await tester.tap(find.text('Romance')); await tester.tap(find.text('Romance'));
@@ -154,7 +165,7 @@ void main() {
await tester.tap(find.byKey(const Key('tab-discover'))); await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle(); 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('Categories could not be loaded.'), findsOneWidget);
expect(find.text('Retry'), findsOneWidget); expect(find.text('Retry'), findsOneWidget);
expect(find.byKey(const Key('home-all-dramas-grid')), findsNothing); expect(find.byKey(const Key('home-all-dramas-grid')), findsNothing);
@@ -176,7 +187,7 @@ void main() {
); );
await tester.pumpAndSettle(); 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(find.text('City Lights & Velvet Nights'), findsOneWidget);
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
+44 -7
View File
@@ -23,24 +23,51 @@ void main() {
); );
expect(find.byKey(const Key('empty-state-art')), findsOneWidget); 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.byKey(const Key('empty-state-icon')), findsOneWidget);
expect(find.text('No stories'), findsOneWidget); expect(find.text('No stories'), findsOneWidget);
expect(find.text('New content will appear here.'), findsOneWidget); expect(find.text('New content will appear here.'), findsOneWidget);
expect( expect(
tester.getSize(find.byKey(const Key('empty-state-art'))), 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( expect(
tester.getSize(find.byKey(const Key('empty-state-icon'))), tester.getSize(find.byKey(const Key('empty-state-icon'))),
const Size(42, 42), const Size(27, 27),
);
final outer = tester.widget<Container>(
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<Container>(
find.byKey(const Key('empty-state-art-inner')),
);
expect((inner.decoration! as BoxDecoration).color?.a, closeTo(0.11, 0.001));
expect(
tester
.widget<Padding>(find.byKey(const Key('empty-state-padding')))
.padding,
const EdgeInsets.symmetric(horizontal: 24, vertical: 28),
); );
expect( expect(
tester tester
.widget<Text>(find.byKey(const Key('empty-state-title'))) .widget<Text>(find.byKey(const Key('empty-state-title')))
.style .style
?.fontSize, ?.fontSize,
16, 15,
); );
final subtitle = tester.widget<Text>(
find.text('New content will appear here.'),
);
expect(subtitle.style?.fontSize, 12.5);
expect(tester.widget<Text>(find.text('Retry')).style?.fontSize, 13);
expect( expect(
tester.getSize(find.byKey(const Key('empty-state-action'))).height, tester.getSize(find.byKey(const Key('empty-state-action'))).height,
greaterThanOrEqualTo(44), greaterThanOrEqualTo(44),
@@ -72,18 +99,28 @@ void main() {
expect( expect(
tester.getSize(find.byKey(const Key('empty-state-art'))), 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( expect(
tester.getSize(find.byKey(const Key('empty-state-icon'))), tester.getSize(find.byKey(const Key('empty-state-icon'))),
const Size(28, 28), const Size(21, 21),
); );
expect( expect(
tester tester
.widget<Text>(find.byKey(const Key('empty-state-title'))) .widget<Text>(find.byKey(const Key('empty-state-title')))
.style .style
?.fontSize, ?.fontSize,
14, 15,
);
expect(
tester
.widget<Padding>(find.byKey(const Key('empty-state-padding')))
.padding,
const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
); );
expect(find.byKey(const Key('empty-state-action')), findsNothing); expect(find.byKey(const Key('empty-state-action')), findsNothing);
}); });
@@ -129,7 +166,7 @@ void main() {
final content = tester.widget<ConstrainedBox>( final content = tester.widget<ConstrainedBox>(
find.byKey(const Key('empty-state-content')), find.byKey(const Key('empty-state-content')),
); );
expect(content.constraints.maxWidth, 280); expect(content.constraints.maxWidth, 260);
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
} }
+17 -11
View File
@@ -155,7 +155,7 @@ void main() {
await tester.pumpWidget(appUnderTest()); await tester.pumpWidget(appUnderTest());
await tester.pumpAndSettle(); 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.binding.handlePopRoute();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
@@ -186,15 +186,17 @@ void main() {
.active, .active,
isFalse, isFalse,
); );
expect(find.text('All Dramas'), findsOneWidget); expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget);
await tester.binding.handlePopRoute(); await tester.binding.handlePopRoute();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('drawer-discover'))); await tester.tap(find.byKey(const Key('drawer-discover')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('All Dramas'), findsOneWidget); expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget);
expect( expect(
Navigator.of(tester.element(find.text('All Dramas'))).canPop(), Navigator.of(
tester.element(find.byKey(const Key('home-all-dramas-grid'))),
).canPop(),
isFalse, isFalse,
); );
}, },
@@ -313,7 +315,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('All'), findsOneWidget); 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('Rewards'), findsOneWidget);
expect(find.text(r'$ 0'), findsOneWidget); expect(find.text(r'$ 0'), findsOneWidget);
expect(find.text('Urban Romance', skipOffstage: false), findsOneWidget); expect(find.text('Urban Romance', skipOffstage: false), findsOneWidget);
@@ -345,10 +347,12 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.text('Home')); await tester.tap(find.text('Home'));
await tester.pumpAndSettle(); 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(find.byTooltip('Search'), findsNothing);
expect( expect(
Navigator.of(tester.element(find.text('All Dramas'))).canPop(), Navigator.of(
tester.element(find.byKey(const Key('home-all-dramas-grid'))),
).canPop(),
isFalse, isFalse,
); );
@@ -365,10 +369,12 @@ void main() {
await tester.tap(find.byKey(const Key('tab-discover'))); await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle(); 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(find.byTooltip('Search'), findsNothing);
expect( expect(
Navigator.of(tester.element(find.text('All Dramas'))).canPop(), Navigator.of(
tester.element(find.byKey(const Key('home-all-dramas-grid'))),
).canPop(),
isFalse, isFalse,
); );
await tester.tap(find.byKey(const Key('tab-my-list'))); await tester.tap(find.byKey(const Key('tab-my-list')));
@@ -564,7 +570,7 @@ void main() {
await tester.pumpWidget(appUnderTest()); await tester.pumpWidget(appUnderTest());
await tester.pumpAndSettle(); 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.binding.handlePopRoute();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('Quick access'), findsOneWidget); expect(find.text('Quick access'), findsOneWidget);
@@ -572,7 +578,7 @@ void main() {
await tester.binding.handlePopRoute(); await tester.binding.handlePopRoute();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('Quick access'), findsNothing); expect(find.text('Quick access'), findsNothing);
expect(find.text('All Dramas'), findsOneWidget); expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget);
expect( expect(
Navigator.of( Navigator.of(
tester.element(find.byKey(const Key('home-tabs-bar'))), tester.element(find.byKey(const Key('home-tabs-bar'))),