feat(flutter): 精简首页标题并重做空态视觉
This commit is contained in:
1 parent
b5a8656ff0
commit
2dd4627bdc
5 files changed
+127
-63
No files matched your search
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
|
||||
@@ -144,24 +144,8 @@ class _DiscoverPageState extends State<DiscoverPage> {
|
||||
),
|
||||
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<DiscoverPage> {
|
||||
(code: 'category-${category.id}', label: category.name),
|
||||
];
|
||||
return SizedBox(
|
||||
key: const Key('home-category-chips'),
|
||||
height: 32,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
|
||||
@@ -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<SliverGrid>(
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -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<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(
|
||||
tester
|
||||
.widget<Text>(find.byKey(const Key('empty-state-title')))
|
||||
.style
|
||||
?.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(
|
||||
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<Text>(find.byKey(const Key('empty-state-title')))
|
||||
.style
|
||||
?.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);
|
||||
});
|
||||
@@ -129,7 +166,7 @@ void main() {
|
||||
final content = tester.widget<ConstrainedBox>(
|
||||
find.byKey(const Key('empty-state-content')),
|
||||
);
|
||||
expect(content.constraints.maxWidth, 280);
|
||||
expect(content.constraints.maxWidth, 260);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
|
||||
+17
-11
@@ -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'))),
|
||||
|
||||
Reference in new issue
Block a user