From 04a1a4ee82dfcb5c1d6806ff845b9c17332c86be Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Tue, 20 Oct 2026 15:13:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(flutter):=20=E6=A6=9C=E5=8D=95=E5=89=8D?= =?UTF-8?q?=E4=B8=89=E9=93=BA=E6=BB=A1=E4=B8=8E=E5=89=A7=E5=9C=BA=E5=88=86?= =?UTF-8?q?=E7=B1=BB=E5=90=B8=E9=A1=B6=E4=B8=8B=E5=88=92=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/discover/discover_view.dart | 45 ++++++--- lib/pages/ranking/ranking_page.dart | 100 ++++++++++++------- test/discover/home_all_dramas_grid_test.dart | 23 ++++- test/discover/theater_pages_test.dart | 14 ++- 4 files changed, 130 insertions(+), 52 deletions(-) diff --git a/lib/pages/discover/discover_view.dart b/lib/pages/discover/discover_view.dart index c5492a4..a74cb56 100644 --- a/lib/pages/discover/discover_view.dart +++ b/lib/pages/discover/discover_view.dart @@ -34,7 +34,7 @@ const double _categoryTabHeight = 44; const double _categoryTabGap = 20; const double _categorySelectedFontSize = 16; const double _categoryFontSize = 15; -const double _stickyHeaderHeight = 102; +const double _stickyHeaderHeight = 44; const Duration _stickyHeaderTransition = Duration(milliseconds: 200); /// K28-A2 剧场页:搜索、分类/语言 chips 与双列剧库。 @@ -203,10 +203,7 @@ class _DiscoverPageState extends State { ] : const [], ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [_categoryTabs(l10n, categories), _toolbar(l10n, categories)], - ), + child: _categoryTabs(l10n, categories), ); } @@ -251,6 +248,7 @@ class _DiscoverPageState extends State { ? categories.feed.dramas : categories.dramasOf(logic.selectedCategoryId!); return [ + _toolbar(l10n, categories), if (dramas.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 40), @@ -279,6 +277,7 @@ class _DiscoverPageState extends State { if (index == 0) { return _categoryTab( key: const Key('theater-category-all'), + id: 'all', label: l10n.all, selected: logic.selectedCategoryId == null, onTap: () => logic.selectCategory(null), @@ -287,6 +286,7 @@ class _DiscoverPageState extends State { final category = categories.categories[index - 1]; return _categoryTab( key: Key('theater-category-${category.id}'), + id: category.id, label: category.name, selected: logic.selectedCategoryId == category.id, onTap: () => logic.selectCategory(category.id), @@ -298,6 +298,7 @@ class _DiscoverPageState extends State { Widget _categoryTab({ required Key key, + required String id, required String label, required bool selected, required VoidCallback onTap, @@ -306,16 +307,32 @@ class _DiscoverPageState extends State { key: key, behavior: HitTestBehavior.opaque, onTap: onTap, - child: Center( - child: Text( - label, - maxLines: 1, - style: TextStyle( - color: selected ? AppColors.onSurface : AppColors.muted, - fontSize: selected ? _categorySelectedFontSize : _categoryFontSize, - fontWeight: selected ? FontWeight.w800 : FontWeight.w500, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + label, + maxLines: 1, + style: TextStyle( + color: selected ? AppColors.onSurface : AppColors.muted, + fontSize: selected + ? _categorySelectedFontSize + : _categoryFontSize, + fontWeight: selected ? FontWeight.w800 : FontWeight.w500, + ), ), - ), + const SizedBox(height: 4), + Container( + key: Key('theater-category-underline-$id'), + width: 18, + height: 2, + decoration: BoxDecoration( + color: selected ? AppColors.blue : Colors.transparent, + borderRadius: BorderRadius.circular(1), + ), + ), + ], ), ); } diff --git a/lib/pages/ranking/ranking_page.dart b/lib/pages/ranking/ranking_page.dart index 34de13b..5e8be44 100644 --- a/lib/pages/ranking/ranking_page.dart +++ b/lib/pages/ranking/ranking_page.dart @@ -16,10 +16,6 @@ import '../drama_detail/drama_detail_view.dart'; const int _topCount = 50; const double _podiumGap = 8; -const double _centerCoverWidth = 120; -const double _centerCoverHeight = 160; -const double _sideCoverWidth = 92; -const double _sideCoverHeight = 124; const double _coverRadius = 12; const double _titleSlotHeight = 24; const double _rowGap = 12; @@ -87,14 +83,13 @@ class _Podium extends StatelessWidget { @override Widget build(BuildContext context) { return Row( - mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ - SizedBox(width: _sideCoverWidth, child: _slot(2)), + Expanded(flex: 100, child: _slot(2)), const SizedBox(width: _podiumGap), - SizedBox(width: _centerCoverWidth, child: _slot(1)), + Expanded(flex: 115, child: _slot(1)), const SizedBox(width: _podiumGap), - SizedBox(width: _sideCoverWidth, child: _slot(3)), + Expanded(flex: 100, child: _slot(3)), ], ); } @@ -118,9 +113,6 @@ class _PodiumCard extends StatelessWidget { bool get _center => rank == 1; - double get _width => _center ? _centerCoverWidth : _sideCoverWidth; - double get _height => _center ? _centerCoverHeight : _sideCoverHeight; - LinearGradient get _badgeGradient => switch (rank) { 1 => const LinearGradient( begin: Alignment.topLeft, @@ -150,10 +142,9 @@ class _PodiumCard extends StatelessWidget { children: [ ClipRRect( borderRadius: BorderRadius.circular(_coverRadius), - child: SizedBox( + child: AspectRatio( key: Key('ranking-cover-$rank'), - width: _width, - height: _height, + aspectRatio: 3 / 4, child: Stack( fit: StackFit.expand, children: [ @@ -228,9 +219,21 @@ class _RankingRow extends StatelessWidget { int get _rank => index + 1; + List _tagValues() { + final result = []; + for (final value in [drama.genre, drama.category]) { + for (final part in value.split(RegExp(r'[,/、·]'))) { + final tag = part.trim(); + if (tag.isEmpty || result.contains(tag)) continue; + result.add(tag); + if (result.length == 3) return result; + } + } + return result; + } + @override Widget build(BuildContext context) { - final genre = drama.genre.isEmpty ? drama.category : drama.genre; return Container( decoration: BoxDecoration( color: AppColors.card, @@ -293,26 +296,46 @@ class _RankingRow extends StatelessWidget { fontWeight: FontWeight.w700, ), ), - if (genre.isNotEmpty) ...[ + if (drama.synopsis.trim().isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + drama.synopsis, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.muted, + fontSize: 12, + height: 1.3, + ), + ), + ], + if (_tagValues().isNotEmpty) ...[ const SizedBox(height: 6), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: AppColors.raised, - borderRadius: BorderRadius.circular(6), - ), - child: Text( - genre, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: AppColors.muted, - fontSize: 11, - ), - ), + Wrap( + spacing: 6, + runSpacing: 4, + children: [ + for (final tag in _tagValues()) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: AppColors.raised, + borderRadius: BorderRadius.circular(6), + ), + child: Text( + tag, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.muted, + fontSize: 11, + ), + ), + ), + ], ), ], const SizedBox(height: 8), @@ -337,6 +360,15 @@ class _RankingRow extends StatelessWidget { ), ), ), + const SizedBox(width: 8), + Text( + l10n.episodeCount(drama.episodeCount), + maxLines: 1, + style: const TextStyle( + color: AppColors.muted, + fontSize: 11, + ), + ), ], ), ], diff --git a/test/discover/home_all_dramas_grid_test.dart b/test/discover/home_all_dramas_grid_test.dart index 19dc45a..05d7fed 100644 --- a/test/discover/home_all_dramas_grid_test.dart +++ b/test/discover/home_all_dramas_grid_test.dart @@ -226,6 +226,27 @@ void main() { expect(header().color, AppColors.ink); }); + testWidgets('should_show_category_underline_for_selection', (tester) async { + await _pumpTheater(tester, categoryRepository: _GridCategoryRepository()); + Color underline(String id) => + (tester + .widget( + find.byKey(Key('theater-category-underline-$id')), + ) + .decoration! + as BoxDecoration) + .color!; + + expect(underline('all'), AppColors.blue); + expect(underline('7'), Colors.transparent); + + await tester.tap(find.byKey(const Key('theater-category-7'))); + await tester.pumpAndSettle(); + + expect(underline('all'), Colors.transparent); + expect(underline('7'), AppColors.blue); + }); + testWidgets('should_use_compact_refresh_indicator', (tester) async { await _pumpTheater(tester, categoryRepository: _GridCategoryRepository()); final indicator = tester.widget( @@ -234,7 +255,7 @@ void main() { expect(indicator.color, AppColors.blue); expect(indicator.strokeWidth, 2); - expect(indicator.edgeOffset, 110); + expect(indicator.edgeOffset, 52); }); } diff --git a/test/discover/theater_pages_test.dart b/test/discover/theater_pages_test.dart index bf6d11b..e0fc3dc 100644 --- a/test/discover/theater_pages_test.dart +++ b/test/discover/theater_pages_test.dart @@ -77,9 +77,10 @@ void main() { final top1 = tester.getRect(find.byKey(const Key('ranking-cover-1'))); final top2 = tester.getRect(find.byKey(const Key('ranking-cover-2'))); final top3 = tester.getRect(find.byKey(const Key('ranking-cover-3'))); - expect(top1.size, const Size(120, 160)); - expect(top2.size, const Size(92, 124)); - expect(top3.size, const Size(92, 124)); + expect(top1.width, greaterThan(top2.width)); + expect(top3.width, closeTo(top2.width, 0.5)); + expect(top2.left, closeTo(14, 0.5)); + expect(top3.right, closeTo(376, 0.5)); expect(top2.left, lessThan(top1.left)); expect(top3.left, greaterThan(top1.right)); expect(top1.bottom, closeTo(top2.bottom, 0.5)); @@ -95,6 +96,13 @@ void main() { expect(find.byKey(const Key('ranking-row-3')), findsOneWidget); expect(find.byKey(const Key('ranking-row-2')), findsNothing); expect(find.byKey(const Key('ranking-top-4')), findsOneWidget); + expect( + find.descendant( + of: find.byKey(const Key('ranking-row-3')), + matching: find.text('synopsis'), + ), + findsOneWidget, + ); }); testWidgets('should_truncate_podium_when_less_than_three', (tester) async {