feat: 增加搜索结果下拉刷新

This commit is contained in:
Zhou Weidong committed 2026-10-22 22:52:50 +08:00
1 parent 281f13a810
commit 6b3d870c93
3 files changed
+65 -11

No files matched your search

+1
View File
@@ -1311,3 +1311,4 @@ $ flutter test
- 搜索页提供返回、自动聚焦输入框与取消,复用 `/app/search` 逻辑和首页三列卡片。 - 搜索页提供返回、自动聚焦输入框与取消,复用 `/app/search` 逻辑和首页三列卡片。
- 清理旧的 searchRequest/focusSearch/syncSearch 参数;`flutter analyze`:No issues found。 - 清理旧的 searchRequest/focusSearch/syncSearch 参数;`flutter analyze`:No issues found。
- 搜索页结果改为独立两列卡片,左右 14、列距 12、行距 16,并移除 HomeFeedCard 复用。 - 搜索页结果改为独立两列卡片,左右 14、列距 12、行距 16,并移除 HomeFeedCard 复用。
- 搜索结果支持下拉刷新:保留旧结果按当前关键词重取第一页,空关键词直接回弹且不改变焦点。
+25 -4
View File
@@ -61,6 +61,14 @@ class DiscoverLogic extends ChangeNotifier {
if (query.isNotEmpty) unawaited(_runSearch(query)); if (query.isNotEmpty) unawaited(_runSearch(query));
} }
/// 用当前关键词重新请求第一页;刷新期间保留旧结果。
Future<void> refreshSearch() async {
final query = search.text.trim();
if (query.isEmpty) return;
_searchTimer?.cancel();
await _runSearch(query, preserveResults: true);
}
void _resetSearch() { void _resetSearch() {
_searchTimer?.cancel(); _searchTimer?.cancel();
_searchRequest++; _searchRequest++;
@@ -85,7 +93,7 @@ class DiscoverLogic extends ChangeNotifier {
_searchTimer = Timer(searchDebounce, () => unawaited(_runSearch(query))); _searchTimer = Timer(searchDebounce, () => unawaited(_runSearch(query)));
} }
Future<void> _runSearch(String query) async { Future<void> _runSearch(String query, {bool preserveResults = false}) async {
final repository = searchRepository; final repository = searchRepository;
if (repository == null) return; if (repository == null) return;
final request = ++_searchRequest; final request = ++_searchRequest;
@@ -101,9 +109,17 @@ class DiscoverLogic extends ChangeNotifier {
if (request != _searchRequest) return; if (request != _searchRequest) return;
searchResults = result.list; searchResults = result.list;
} on ApiException catch (exception) { } on ApiException catch (exception) {
_applySearchError(request, exception.message); _applySearchError(
request,
exception.message,
preserveResults: preserveResults,
);
} catch (_) { } catch (_) {
_applySearchError(request, 'searchFailed'); _applySearchError(
request,
'searchFailed',
preserveResults: preserveResults,
);
} finally { } finally {
if (request == _searchRequest) { if (request == _searchRequest) {
searchLoading = false; searchLoading = false;
@@ -112,8 +128,13 @@ class DiscoverLogic extends ChangeNotifier {
} }
} }
void _applySearchError(int request, String message) { void _applySearchError(
int request,
String message, {
bool preserveResults = false,
}) {
if (request != _searchRequest) return; if (request != _searchRequest) return;
if (preserveResults && searchResults.isNotEmpty) return;
searchError = message; searchError = message;
searchResults = const []; searchResults = const [];
} }
+39 -7
View File
@@ -11,6 +11,7 @@ import '../../core/formatting/error_text.dart';
import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors.dart';
import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_dimens.dart';
import '../../core/theme/app_theme.dart'; import '../../core/theme/app_theme.dart';
import '../../core/widgets/app_pull_to_refresh.dart';
import '../../core/widgets/empty_state.dart'; import '../../core/widgets/empty_state.dart';
import '../../l10n/app_localizations.dart'; import '../../l10n/app_localizations.dart';
import '../player/open_player.dart'; import '../player/open_player.dart';
@@ -64,7 +65,13 @@ class _SearchPageState extends State<SearchPage> {
child: Column( child: Column(
children: [ children: [
_header(l10n), _header(l10n),
Expanded(child: _body(l10n)), Expanded(
child: AppPullToRefresh(
key: const Key('search-refresh'),
onRefresh: _logic.refreshSearch,
child: _body(l10n),
),
),
], ],
), ),
), ),
@@ -182,15 +189,20 @@ class _SearchPageState extends State<SearchPage> {
Widget _body(AppLocalizations l10n) { Widget _body(AppLocalizations l10n) {
if (!_logic.hasSearchQuery) { if (!_logic.hasSearchQuery) {
return EmptyState( return _stateScroll(
EmptyState(
key: const Key('search-idle'), key: const Key('search-idle'),
icon: Icons.search_rounded, icon: Icons.search_rounded,
title: l10n.searchDiscover, title: l10n.searchDiscover,
),
); );
} }
if (_logic.searchResults.isNotEmpty) {
return _resultsGrid();
}
if (_logic.searchLoading) { if (_logic.searchLoading) {
return const Center( return _stateScroll(
child: SizedBox.square( const SizedBox.square(
key: Key('search-loading'), key: Key('search-loading'),
dimension: 20, dimension: 20,
child: CircularProgressIndicator( child: CircularProgressIndicator(
@@ -201,22 +213,41 @@ class _SearchPageState extends State<SearchPage> {
); );
} }
if (_logic.searchError != null) { if (_logic.searchError != null) {
return EmptyState( return _stateScroll(
EmptyState(
key: const Key('search-error'), key: const Key('search-error'),
icon: Icons.cloud_off_rounded, icon: Icons.cloud_off_rounded,
title: errorText(l10n, _logic.searchError, l10n.searchFailed), title: errorText(l10n, _logic.searchError, l10n.searchFailed),
actionLabel: l10n.retry, actionLabel: l10n.retry,
onAction: _logic.retrySearch, onAction: _logic.retrySearch,
),
); );
} }
if (_logic.searchResults.isEmpty) { return _stateScroll(
return EmptyState( EmptyState(
key: const Key('search-empty'), key: const Key('search-empty'),
icon: Icons.search_off_rounded, icon: Icons.search_off_rounded,
title: l10n.noSearchResults, title: l10n.noSearchResults,
subtitle: l10n.emptyHint, subtitle: l10n.emptyHint,
),
); );
} }
Widget _stateScroll(Widget child) {
return LayoutBuilder(
builder: (context, constraints) => ListView(
physics: AppPullToRefresh.physics,
children: [
SizedBox(
height: constraints.maxHeight,
child: Center(child: child),
),
],
),
);
}
Widget _resultsGrid() {
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final width = final width =
@@ -229,6 +260,7 @@ class _SearchPageState extends State<SearchPage> {
AppDimens.searchResultTextExtent; AppDimens.searchResultTextExtent;
return GridView.builder( return GridView.builder(
key: const Key('search-results-grid'), key: const Key('search-results-grid'),
physics: AppPullToRefresh.physics,
padding: const EdgeInsets.fromLTRB( padding: const EdgeInsets.fromLTRB(
AppDimens.feedPageHorizontal, AppDimens.feedPageHorizontal,
AppDimens.spaceMd, AppDimens.spaceMd,