From 6b3d870c93a35a46c07c460af9212ee9cec1ec7d Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Thu, 22 Oct 2026 22:52:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E4=B8=8B=E6=8B=89=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plans/PROGRESS.md | 1 + lib/pages/discover/discover_logic.dart | 29 +++++++++-- lib/pages/discover/search_page.dart | 66 +++++++++++++++++++------- 3 files changed, 75 insertions(+), 21 deletions(-) diff --git a/docs/plans/PROGRESS.md b/docs/plans/PROGRESS.md index 0bd6326..7641b72 100644 --- a/docs/plans/PROGRESS.md +++ b/docs/plans/PROGRESS.md @@ -1311,3 +1311,4 @@ $ flutter test - 搜索页提供返回、自动聚焦输入框与取消,复用 `/app/search` 逻辑和首页三列卡片。 - 清理旧的 searchRequest/focusSearch/syncSearch 参数;`flutter analyze`:No issues found。 - 搜索页结果改为独立两列卡片,左右 14、列距 12、行距 16,并移除 HomeFeedCard 复用。 +- 搜索结果支持下拉刷新:保留旧结果按当前关键词重取第一页,空关键词直接回弹且不改变焦点。 diff --git a/lib/pages/discover/discover_logic.dart b/lib/pages/discover/discover_logic.dart index 7a96911..c2d9f53 100644 --- a/lib/pages/discover/discover_logic.dart +++ b/lib/pages/discover/discover_logic.dart @@ -61,6 +61,14 @@ class DiscoverLogic extends ChangeNotifier { if (query.isNotEmpty) unawaited(_runSearch(query)); } + /// 用当前关键词重新请求第一页;刷新期间保留旧结果。 + Future refreshSearch() async { + final query = search.text.trim(); + if (query.isEmpty) return; + _searchTimer?.cancel(); + await _runSearch(query, preserveResults: true); + } + void _resetSearch() { _searchTimer?.cancel(); _searchRequest++; @@ -85,7 +93,7 @@ class DiscoverLogic extends ChangeNotifier { _searchTimer = Timer(searchDebounce, () => unawaited(_runSearch(query))); } - Future _runSearch(String query) async { + Future _runSearch(String query, {bool preserveResults = false}) async { final repository = searchRepository; if (repository == null) return; final request = ++_searchRequest; @@ -101,9 +109,17 @@ class DiscoverLogic extends ChangeNotifier { if (request != _searchRequest) return; searchResults = result.list; } on ApiException catch (exception) { - _applySearchError(request, exception.message); + _applySearchError( + request, + exception.message, + preserveResults: preserveResults, + ); } catch (_) { - _applySearchError(request, 'searchFailed'); + _applySearchError( + request, + 'searchFailed', + preserveResults: preserveResults, + ); } finally { if (request == _searchRequest) { 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 (preserveResults && searchResults.isNotEmpty) return; searchError = message; searchResults = const []; } diff --git a/lib/pages/discover/search_page.dart b/lib/pages/discover/search_page.dart index c78741a..cf086ef 100644 --- a/lib/pages/discover/search_page.dart +++ b/lib/pages/discover/search_page.dart @@ -11,6 +11,7 @@ import '../../core/formatting/error_text.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_theme.dart'; +import '../../core/widgets/app_pull_to_refresh.dart'; import '../../core/widgets/empty_state.dart'; import '../../l10n/app_localizations.dart'; import '../player/open_player.dart'; @@ -64,7 +65,13 @@ class _SearchPageState extends State { child: Column( children: [ _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 { Widget _body(AppLocalizations l10n) { if (!_logic.hasSearchQuery) { - return EmptyState( - key: const Key('search-idle'), - icon: Icons.search_rounded, - title: l10n.searchDiscover, + return _stateScroll( + EmptyState( + key: const Key('search-idle'), + icon: Icons.search_rounded, + title: l10n.searchDiscover, + ), ); } + if (_logic.searchResults.isNotEmpty) { + return _resultsGrid(); + } if (_logic.searchLoading) { - return const Center( - child: SizedBox.square( + return _stateScroll( + const SizedBox.square( key: Key('search-loading'), dimension: 20, child: CircularProgressIndicator( @@ -201,22 +213,41 @@ class _SearchPageState extends State { ); } if (_logic.searchError != null) { - return EmptyState( - key: const Key('search-error'), - icon: Icons.cloud_off_rounded, - title: errorText(l10n, _logic.searchError, l10n.searchFailed), - actionLabel: l10n.retry, - onAction: _logic.retrySearch, + return _stateScroll( + EmptyState( + key: const Key('search-error'), + icon: Icons.cloud_off_rounded, + title: errorText(l10n, _logic.searchError, l10n.searchFailed), + actionLabel: l10n.retry, + onAction: _logic.retrySearch, + ), ); } - if (_logic.searchResults.isEmpty) { - return EmptyState( + return _stateScroll( + EmptyState( key: const Key('search-empty'), icon: Icons.search_off_rounded, title: l10n.noSearchResults, 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( builder: (context, constraints) { final width = @@ -229,6 +260,7 @@ class _SearchPageState extends State { AppDimens.searchResultTextExtent; return GridView.builder( key: const Key('search-results-grid'), + physics: AppPullToRefresh.physics, padding: const EdgeInsets.fromLTRB( AppDimens.feedPageHorizontal, AppDimens.spaceMd,