merge: 调整搜索结果两列布局
This commit is contained in:
commit
281f13a810
6 files changed
+124
-9
No files matched your search
@@ -1310,3 +1310,4 @@ $ flutter test
|
|||||||
- 首页与推荐/剧单/全部短剧搜索入口统一 push SearchPage,不再切换推荐 tab 聚焦。
|
- 首页与推荐/剧单/全部短剧搜索入口统一 push SearchPage,不再切换推荐 tab 聚焦。
|
||||||
- 搜索页提供返回、自动聚焦输入框与取消,复用 `/app/search` 逻辑和首页三列卡片。
|
- 搜索页提供返回、自动聚焦输入框与取消,复用 `/app/search` 逻辑和首页三列卡片。
|
||||||
- 清理旧的 searchRequest/focusSearch/syncSearch 参数;`flutter analyze`:No issues found。
|
- 清理旧的 searchRequest/focusSearch/syncSearch 参数;`flutter analyze`:No issues found。
|
||||||
|
- 搜索页结果改为独立两列卡片,左右 14、列距 12、行距 16,并移除 HomeFeedCard 复用。
|
||||||
@@ -112,6 +112,7 @@ abstract final class AppColors {
|
|||||||
static const textTertiary = Color(0xFF8E8E93);
|
static const textTertiary = Color(0xFF8E8E93);
|
||||||
static const textTabInactive = Color(0xFFB0B0B0);
|
static const textTabInactive = Color(0xFFB0B0B0);
|
||||||
static const textFilterIcon = Color(0xFF7A7A7E);
|
static const textFilterIcon = Color(0xFF7A7A7E);
|
||||||
|
static const searchResultMeta = Color(0xFF9E9E9E);
|
||||||
|
|
||||||
/// 原生开屏与冷启动过渡底色。
|
/// 原生开屏与冷启动过渡底色。
|
||||||
static const splashInk = ink;
|
static const splashInk = ink;
|
||||||
|
|||||||
@@ -62,6 +62,12 @@ abstract final class AppDimens {
|
|||||||
static const double searchHeight = 40;
|
static const double searchHeight = 40;
|
||||||
static const double searchRadius = 8;
|
static const double searchRadius = 8;
|
||||||
static const double searchPlaceholderFont = 13;
|
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 navDot = 3;
|
||||||
static const double glassBlur = 24;
|
static const double glassBlur = 24;
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ import '../../core/theme/app_dimens.dart';
|
|||||||
import '../../core/theme/app_theme.dart';
|
import '../../core/theme/app_theme.dart';
|
||||||
import '../../core/widgets/empty_state.dart';
|
import '../../core/widgets/empty_state.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
import '../home/widgets/home_feed_card.dart';
|
|
||||||
import '../player/open_player.dart';
|
import '../player/open_player.dart';
|
||||||
import 'discover_binding.dart';
|
import 'discover_binding.dart';
|
||||||
import 'discover_logic.dart';
|
import 'discover_logic.dart';
|
||||||
|
import 'widgets/search_result_card.dart';
|
||||||
|
|
||||||
class SearchPage extends StatefulWidget {
|
class SearchPage extends StatefulWidget {
|
||||||
const SearchPage({super.key});
|
const SearchPage({super.key});
|
||||||
@@ -222,11 +222,11 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
final width =
|
final width =
|
||||||
(constraints.maxWidth -
|
(constraints.maxWidth -
|
||||||
AppDimens.feedPageHorizontal * 2 -
|
AppDimens.feedPageHorizontal * 2 -
|
||||||
AppDimens.homeGridGap * 2) /
|
AppDimens.searchResultGap) /
|
||||||
3;
|
2;
|
||||||
final itemExtent =
|
final itemExtent =
|
||||||
width / AppDimens.cardCoverAspectRatio +
|
width / AppDimens.cardCoverAspectRatio +
|
||||||
AppDimens.homeCardBodyExtent;
|
AppDimens.searchResultTextExtent;
|
||||||
return GridView.builder(
|
return GridView.builder(
|
||||||
key: const Key('search-results-grid'),
|
key: const Key('search-results-grid'),
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: const EdgeInsets.fromLTRB(
|
||||||
@@ -236,15 +236,15 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
AppDimens.spaceXl,
|
AppDimens.spaceXl,
|
||||||
),
|
),
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 3,
|
crossAxisCount: 2,
|
||||||
crossAxisSpacing: AppDimens.homeGridGap,
|
crossAxisSpacing: AppDimens.searchResultGap,
|
||||||
mainAxisSpacing: AppDimens.homeGridRowGap,
|
mainAxisSpacing: AppDimens.searchResultRowGap,
|
||||||
mainAxisExtent: itemExtent,
|
mainAxisExtent: itemExtent,
|
||||||
),
|
),
|
||||||
itemCount: _logic.searchResults.length,
|
itemCount: _logic.searchResults.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final drama = _logic.searchResults[index];
|
final drama = _logic.searchResults[index];
|
||||||
return HomeFeedCard(
|
return SearchResultCard(
|
||||||
drama: drama,
|
drama: drama,
|
||||||
onTap: () => unawaited(playDramaFromCatalog(context, drama)),
|
onTap: () => unawaited(playDramaFromCatalog(context, drama)),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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<String> _tags() {
|
||||||
|
final seen = <String>{};
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -146,7 +146,10 @@ void main() {
|
|||||||
);
|
);
|
||||||
final delegate =
|
final delegate =
|
||||||
results.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount;
|
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.tap(find.byKey(const Key('search-back')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.byKey(const Key('recommend-page')), findsOneWidget);
|
expect(find.byKey(const Key('recommend-page')), findsOneWidget);
|
||||||
|
|||||||
Reference in new issue
Block a user