diff --git a/docs/plans/PROGRESS.md b/docs/plans/PROGRESS.md index 4ca1922..0bd6326 100644 --- a/docs/plans/PROGRESS.md +++ b/docs/plans/PROGRESS.md @@ -1310,3 +1310,4 @@ $ flutter test - 首页与推荐/剧单/全部短剧搜索入口统一 push SearchPage,不再切换推荐 tab 聚焦。 - 搜索页提供返回、自动聚焦输入框与取消,复用 `/app/search` 逻辑和首页三列卡片。 - 清理旧的 searchRequest/focusSearch/syncSearch 参数;`flutter analyze`:No issues found。 +- 搜索页结果改为独立两列卡片,左右 14、列距 12、行距 16,并移除 HomeFeedCard 复用。 diff --git a/lib/core/theme/app_colors.dart b/lib/core/theme/app_colors.dart index 56ab3f8..a3fb544 100644 --- a/lib/core/theme/app_colors.dart +++ b/lib/core/theme/app_colors.dart @@ -112,6 +112,7 @@ abstract final class AppColors { static const textTertiary = Color(0xFF8E8E93); static const textTabInactive = Color(0xFFB0B0B0); static const textFilterIcon = Color(0xFF7A7A7E); + static const searchResultMeta = Color(0xFF9E9E9E); /// 原生开屏与冷启动过渡底色。 static const splashInk = ink; diff --git a/lib/core/theme/app_dimens.dart b/lib/core/theme/app_dimens.dart index 67ddd3e..c127122 100644 --- a/lib/core/theme/app_dimens.dart +++ b/lib/core/theme/app_dimens.dart @@ -62,6 +62,12 @@ abstract final class AppDimens { static const double searchHeight = 40; static const double searchRadius = 8; static const double searchPlaceholderFont = 13; + static const double searchResultGap = 12; + static const double searchResultRowGap = 16; + static const double searchResultTitleGap = 8; + static const double searchResultMetaGap = 6; + static const double searchResultMetaFont = 12; + static const double searchResultTextExtent = 68; static const double navDot = 3; static const double glassBlur = 24; diff --git a/lib/pages/discover/search_page.dart b/lib/pages/discover/search_page.dart index bc4ac1c..c78741a 100644 --- a/lib/pages/discover/search_page.dart +++ b/lib/pages/discover/search_page.dart @@ -13,10 +13,10 @@ import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_theme.dart'; import '../../core/widgets/empty_state.dart'; import '../../l10n/app_localizations.dart'; -import '../home/widgets/home_feed_card.dart'; import '../player/open_player.dart'; import 'discover_binding.dart'; import 'discover_logic.dart'; +import 'widgets/search_result_card.dart'; class SearchPage extends StatefulWidget { const SearchPage({super.key}); @@ -222,11 +222,11 @@ class _SearchPageState extends State { final width = (constraints.maxWidth - AppDimens.feedPageHorizontal * 2 - - AppDimens.homeGridGap * 2) / - 3; + AppDimens.searchResultGap) / + 2; final itemExtent = width / AppDimens.cardCoverAspectRatio + - AppDimens.homeCardBodyExtent; + AppDimens.searchResultTextExtent; return GridView.builder( key: const Key('search-results-grid'), padding: const EdgeInsets.fromLTRB( @@ -236,15 +236,15 @@ class _SearchPageState extends State { AppDimens.spaceXl, ), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - crossAxisSpacing: AppDimens.homeGridGap, - mainAxisSpacing: AppDimens.homeGridRowGap, + crossAxisCount: 2, + crossAxisSpacing: AppDimens.searchResultGap, + mainAxisSpacing: AppDimens.searchResultRowGap, mainAxisExtent: itemExtent, ), itemCount: _logic.searchResults.length, itemBuilder: (context, index) { final drama = _logic.searchResults[index]; - return HomeFeedCard( + return SearchResultCard( drama: drama, onTap: () => unawaited(playDramaFromCatalog(context, drama)), ); diff --git a/lib/pages/discover/widgets/search_result_card.dart b/lib/pages/discover/widgets/search_result_card.dart new file mode 100644 index 0000000..ad6043f --- /dev/null +++ b/lib/pages/discover/widgets/search_result_card.dart @@ -0,0 +1,104 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; + +import '../../../core/data/models/drama.dart'; +import '../../../core/theme/app_colors.dart'; +import '../../../core/theme/app_dimens.dart'; +import '../../../core/widgets/cover_play_count.dart'; +import '../../../core/widgets/remote_image.dart'; + +class SearchResultCard extends StatelessWidget { + const SearchResultCard({super.key, required this.drama, required this.onTap}); + + final Drama drama; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final tags = _tags(); + return Material( + key: Key('search-result-card-${drama.id}'), + color: AppColors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(AppDimens.cardCoverRadius), + onTap: onTap, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(AppDimens.cardCoverRadius), + child: AspectRatio( + key: Key('search-result-cover-${drama.id}'), + aspectRatio: AppDimens.cardCoverAspectRatio, + child: Stack( + fit: StackFit.expand, + children: [ + RemoteImage( + drama.posterUrl, + fit: BoxFit.cover, + showFallbackIcon: true, + fallbackColor: AppColors.contentSurface, + ), + const Positioned( + left: 0, + right: 0, + bottom: 0, + child: CoverBottomScrim(), + ), + Positioned( + right: AppDimens.cardPlayCountRight, + bottom: AppDimens.cardPlayCountBottom, + child: CoverPlayCount( + key: Key('search-result-views-${drama.id}'), + count: drama.viewCount, + ), + ), + ], + ), + ), + ), + const SizedBox(height: AppDimens.searchResultTitleGap), + Text( + drama.title, + key: Key('search-result-title-${drama.id}'), + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.left, + style: const TextStyle( + color: AppColors.textPrimary, + fontSize: AppDimens.cardTitleFont, + fontWeight: FontWeight.w500, + height: AppDimens.cardTitleLineHeight, + ), + ), + if (tags.isNotEmpty) ...[ + const SizedBox(height: AppDimens.searchResultMetaGap), + Text( + tags.join(' | '), + key: Key('search-result-tags-${drama.id}'), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.left, + style: const TextStyle( + color: AppColors.searchResultMeta, + fontSize: AppDimens.searchResultMetaFont, + ), + ), + ], + ], + ), + ), + ); + } + + List _tags() { + final seen = {}; + return [drama.genre, drama.category] + .expand((value) => value.split(RegExp(r'[,/、·]'))) + .map((value) => value.trim()) + .where((value) => value.isNotEmpty && seen.add(value)) + .take(3) + .toList(growable: false); + } +} diff --git a/test/home/k28_a_navigation_test.dart b/test/home/k28_a_navigation_test.dart index 5e5eda7..ae8b830 100644 --- a/test/home/k28_a_navigation_test.dart +++ b/test/home/k28_a_navigation_test.dart @@ -146,7 +146,10 @@ void main() { ); final delegate = results.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount; - expect(delegate.crossAxisCount, 3); + expect(delegate.crossAxisCount, 2); + expect(delegate.crossAxisSpacing, 12); + expect(delegate.mainAxisSpacing, 16); + expect(results.padding, const EdgeInsets.fromLTRB(14, 12, 14, 24)); await tester.tap(find.byKey(const Key('search-back'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('recommend-page')), findsOneWidget);