From 4eb411ef289f378754980678f13c6924e7d41001 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Thu, 22 Oct 2026 20:39:00 +0800 Subject: [PATCH] =?UTF-8?q?style(recommend):=20=E9=87=8D=E6=8E=92=E5=85=A8?= =?UTF-8?q?=E9=83=A8=E7=9F=AD=E5=89=A7=E7=AD=9B=E9=80=89=E4=B8=8E=E7=BD=91?= =?UTF-8?q?=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plans/PROGRESS.md | 6 + lib/core/theme/app_colors.dart | 1 + lib/core/theme/app_dimens.dart | 12 +- .../widgets/all_dramas_filter_grid.dart | 169 +++++++++++------- test/recommend/recommend_sections_test.dart | 47 +++++ 5 files changed, 170 insertions(+), 65 deletions(-) diff --git a/docs/plans/PROGRESS.md b/docs/plans/PROGRESS.md index a351bdd..7146751 100644 --- a/docs/plans/PROGRESS.md +++ b/docs/plans/PROGRESS.md @@ -1206,3 +1206,9 @@ $ flutter test - 推荐、剧单、全部短剧改为共享 PageController 的横向 PageView,点击与滑动均驱动同一选中态。 - 三页使用 AutomaticKeepAliveClientMixin 保留视频、滚动位置和筛选状态;顶部导航固定在外层。 - `flutter analyze`:No issues found。 + +## 2026-10-22 全部短剧布局重排 + +- 四行筛选统一 28 高、8 行距、24 项目间距;选中 chip 改为 `#4A1E1E`、白字 13/w600、圆角 14。 +- 最热/最新改为纯文字选中态,三横线筛选固定在右端;网格改为左右 12、列距 6、行距 12。 +- 第三行筛选完整展示,卡片继续复用首页组件;`flutter analyze`:No issues found。 diff --git a/lib/core/theme/app_colors.dart b/lib/core/theme/app_colors.dart index c7aca6f..5dbe249 100644 --- a/lib/core/theme/app_colors.dart +++ b/lib/core/theme/app_colors.dart @@ -77,6 +77,7 @@ abstract final class AppColors { static const contentSurface = Color(0xFF1C1C1E); static const contentChip = Color(0xFF2C2C2E); static const contentBorder = Color(0x1AFFFFFF); + static const filterChipSelected = Color(0xFF4A1E1E); /// 「我的」页卡片与描边。 static const mineCard = Color(0xFF16161A); diff --git a/lib/core/theme/app_dimens.dart b/lib/core/theme/app_dimens.dart index f0ba1fe..2e6cb4a 100644 --- a/lib/core/theme/app_dimens.dart +++ b/lib/core/theme/app_dimens.dart @@ -138,8 +138,16 @@ abstract final class AppDimens { /// 全部短剧筛选与追剧双列。 static const double filterChipFont = 13; - static const double filterChipHeight = 30; - static const double filterChipRadius = 15; + static const double filterChipHeight = 28; + static const double filterChipRadius = 14; + static const double filterChipHorizontalPadding = 12; + static const double filterRowGap = 8; + static const double filterItemGap = 24; + static const double allDramasHorizontal = 12; + static const double allDramasGridGap = 6; + static const double allDramasGridRowGap = 12; + static const double allDramasFilterToGridGap = 12; + static const double allDramasFilterIcon = 26; static const double followingTabFont = 16; static const double followingGridAspectRatio = 0.5; diff --git a/lib/pages/recommend/widgets/all_dramas_filter_grid.dart b/lib/pages/recommend/widgets/all_dramas_filter_grid.dart index 059e47b..102f05a 100644 --- a/lib/pages/recommend/widgets/all_dramas_filter_grid.dart +++ b/lib/pages/recommend/widgets/all_dramas_filter_grid.dart @@ -49,8 +49,8 @@ class _AllDramasFilterGridState extends State { builder: (context, constraints) { final itemWidth = (constraints.maxWidth - - AppDimens.feedPageHorizontal * 2 - - AppDimens.homeGridGap * 2) / + AppDimens.allDramasHorizontal * 2 - + AppDimens.allDramasGridGap * 2) / 3; final itemExtent = itemWidth / AppDimens.cardCoverAspectRatio + @@ -85,14 +85,14 @@ class _AllDramasFilterGridState extends State { else SliverPadding( padding: const EdgeInsets.symmetric( - horizontal: AppDimens.feedPageHorizontal, + horizontal: AppDimens.allDramasHorizontal, ), sliver: SliverGrid( key: const Key('all-dramas-grid'), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, - crossAxisSpacing: AppDimens.homeGridGap, - mainAxisSpacing: AppDimens.homeGridRowGap, + crossAxisSpacing: AppDimens.allDramasGridGap, + mainAxisSpacing: AppDimens.allDramasGridRowGap, mainAxisExtent: itemExtent, ), delegate: SliverChildBuilderDelegate( @@ -131,9 +131,9 @@ class _AllDramasFilterGridState extends State { return Padding( padding: const EdgeInsets.fromLTRB( AppDimens.feedPageHorizontal, - AppDimens.spaceSm, + 0, AppDimens.feedPageHorizontal, - AppDimens.spaceMd, + AppDimens.allDramasFilterToGridGap, ), child: Column( key: const Key('all-dramas-filters'), @@ -160,38 +160,7 @@ class _AllDramasFilterGridState extends State { onSelected: (value) => setState(() => _audio = value), ), _genreRow(2, genres, l10n), - Row( - key: const Key('all-filter-row-3'), - children: [ - Expanded( - child: Row( - children: [ - _chip( - key: const Key('all-sort-hot'), - label: l10n.hot, - selected: !_newest, - onTap: () => setState(() => _newest = false), - ), - const SizedBox(width: AppDimens.spaceSm), - _chip( - key: const Key('all-sort-new'), - label: l10n.newest, - selected: _newest, - onTap: () => setState(() => _newest = true), - ), - ], - ), - ), - IconButton( - key: const Key('all-dramas-filter-page'), - tooltip: l10n.filter, - onPressed: widget.onOpenFilter, - color: AppColors.textPrimary, - iconSize: AppDimens.tapTarget / 2, - icon: const Icon(Icons.filter_list_rounded), - ), - ], - ), + _sortRow(l10n), ], ), ); @@ -205,21 +174,26 @@ class _AllDramasFilterGridState extends State { }) { return Padding( key: Key('all-filter-row-$row'), - padding: const EdgeInsets.only(bottom: AppDimens.spaceSm), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - for (final option in options) ...[ - _chip( - key: Key('all-filter-${option.id}'), - label: option.label, - selected: selected == option.id, - onTap: () => onSelected(option.id), - ), - const SizedBox(width: AppDimens.spaceSm), + padding: const EdgeInsets.only(bottom: AppDimens.filterRowGap), + child: SizedBox( + height: AppDimens.filterChipHeight, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + for (var index = 0; index < options.length; index++) ...[ + _chip( + key: Key('all-filter-${options[index].id}'), + label: options[index].label, + selected: selected == options[index].id, + onTap: () => onSelected(options[index].id), + ), + if (index < options.length - 1) + const SizedBox(width: AppDimens.filterItemGap), + ], ], - ], + ), ), ), ); @@ -254,18 +228,93 @@ class _AllDramasFilterGridState extends State { borderRadius: BorderRadius.circular(AppDimens.filterChipRadius), child: Container( height: AppDimens.filterChipHeight, - padding: const EdgeInsets.symmetric(horizontal: AppDimens.spaceMd), + padding: const EdgeInsets.symmetric( + horizontal: AppDimens.filterChipHorizontalPadding, + ), alignment: Alignment.center, decoration: BoxDecoration( - color: selected ? AppColors.contentSurface : AppColors.transparent, + color: selected + ? AppColors.filterChipSelected + : AppColors.transparent, borderRadius: BorderRadius.circular(AppDimens.filterChipRadius), ), child: Text( label, style: TextStyle( - color: selected ? AppColors.textPrimary : AppColors.textTertiary, + color: selected ? AppColors.textPrimary : AppColors.textTabInactive, fontSize: AppDimens.filterChipFont, - fontWeight: selected ? FontWeight.w700 : FontWeight.w400, + fontWeight: selected ? FontWeight.w600 : FontWeight.w400, + height: 1, + ), + ), + ), + ); + } + + Widget _sortRow(AppLocalizations l10n) { + return SizedBox( + key: const Key('all-filter-row-3'), + height: AppDimens.filterChipHeight, + child: Row( + children: [ + _sortOption( + key: const Key('all-sort-hot'), + label: l10n.hot, + selected: !_newest, + onTap: () => setState(() => _newest = false), + ), + const SizedBox(width: AppDimens.filterItemGap), + _sortOption( + key: const Key('all-sort-new'), + label: l10n.newest, + selected: _newest, + onTap: () => setState(() => _newest = true), + ), + const Spacer(), + SizedBox.square( + dimension: AppDimens.allDramasFilterIcon, + child: IconButton( + key: const Key('all-dramas-filter-page'), + tooltip: l10n.filter, + padding: EdgeInsets.zero, + constraints: const BoxConstraints.tightFor( + width: AppDimens.allDramasFilterIcon, + height: AppDimens.allDramasFilterIcon, + ), + onPressed: widget.onOpenFilter, + color: AppColors.textPrimary, + iconSize: AppDimens.allDramasFilterIcon, + icon: const Icon(Icons.filter_list_rounded), + ), + ), + ], + ), + ); + } + + Widget _sortOption({ + required Key key, + required String label, + required bool selected, + required VoidCallback onTap, + }) { + return GestureDetector( + key: key, + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: SizedBox( + height: AppDimens.filterChipHeight, + child: Center( + child: Text( + label, + style: TextStyle( + color: selected + ? AppColors.textPrimary + : AppColors.textTabInactive, + fontSize: AppDimens.filterChipFont, + fontWeight: selected ? FontWeight.w700 : FontWeight.w400, + height: 1, + ), ), ), ), @@ -273,7 +322,6 @@ class _AllDramasFilterGridState extends State { } List<({String id, String label})> _availableGenres(AppLocalizations l10n) { - final source = widget.feedController.dramas; final definitions = [ (id: 'reversal', label: l10n.reversalFilter, terms: _reversalTerms), (id: 'uplifting', label: l10n.homeTagUplifting, terms: []), @@ -282,11 +330,6 @@ class _AllDramasFilterGridState extends State { (id: 'angst', label: l10n.homeTagAngst, terms: []), ]; return definitions - .where( - (definition) => source.any( - (drama) => _matchesGenre(drama, definition.id, definition.terms), - ), - ) .map((definition) => (id: definition.id, label: definition.label)) .toList(growable: false); } diff --git a/test/recommend/recommend_sections_test.dart b/test/recommend/recommend_sections_test.dart index fa50057..132301a 100644 --- a/test/recommend/recommend_sections_test.dart +++ b/test/recommend/recommend_sections_test.dart @@ -237,14 +237,49 @@ void main() { await tester.tap(find.byKey(const Key('recommend-tab-all'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('all-dramas-grid')), findsOneWidget); + final grid = tester.widget( + find.byKey(const Key('all-dramas-grid')), + ); + final delegate = + grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount; + expect(delegate.crossAxisCount, 3); + expect(delegate.crossAxisSpacing, AppDimens.allDramasGridGap); + expect(delegate.mainAxisSpacing, AppDimens.allDramasGridRowGap); for (var row = 0; row < 4; row++) { expect(find.byKey(Key('all-filter-row-$row')), findsOneWidget); } expect(find.byKey(const Key('all-filter-local')), findsOneWidget); expect(find.byKey(const Key('all-filter-subtitles')), findsOneWidget); + for (final key in [ + 'all-filter-reversal', + 'all-filter-uplifting', + 'all-filter-modern', + 'all-filter-sweet', + 'all-filter-angst', + ]) { + expect(find.byKey(Key(key)), findsOneWidget); + } await tester.tap(find.byKey(const Key('all-filter-local'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('all-dramas-empty')), findsOneWidget); + expect(_textStyle(tester, 'Local').fontWeight, FontWeight.w600); + expect(_textStyle(tester, 'Local').color, AppColors.textPrimary); + final selectedChip = tester.widget( + find + .descendant( + of: find.byKey(const Key('all-filter-local')), + matching: find.byType(Container), + ) + .first, + ); + expect( + (selectedChip.decoration! as BoxDecoration).color, + AppColors.filterChipSelected, + ); + expect( + tester.getSize(find.byKey(const Key('all-filter-local'))).height, + AppDimens.filterChipHeight, + ); await tester.tap(find.byKey(const Key('all-filter-all')).first); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('all-sort-new'))); @@ -265,6 +300,18 @@ void main() { (chipContainer.decoration! as BoxDecoration).borderRadius, BorderRadius.circular(AppDimens.filterChipRadius), ); + final filterButton = find.byKey(const Key('all-dramas-filter-page')); + expect( + tester.getSize(filterButton), + const Size.square(AppDimens.allDramasFilterIcon), + ); + expect( + tester.getRect(filterButton).right, + closeTo( + tester.getRect(find.byKey(const Key('all-filter-row-3'))).right, + 0.1, + ), + ); await tester.tap(find.byKey(const Key('recommend-tab-playlist'))); await tester.pumpAndSettle();