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

This commit is contained in:
Zhou Weidong committed 2026-10-22 22:53:01 +08:00
commit 928ac1de19
3 files changed
+75 -21

No files matched your search

+1
View File
@@ -1311,3 +1311,4 @@ $ flutter test
- 搜索页提供返回、自动聚焦输入框与取消,复用 `/app/search` 逻辑和首页三列卡片。
- 清理旧的 searchRequest/focusSearch/syncSearch 参数;`flutter analyze`:No issues found。
- 搜索页结果改为独立两列卡片,左右 14、列距 12、行距 16,并移除 HomeFeedCard 复用。
- 搜索结果支持下拉刷新:保留旧结果按当前关键词重取第一页,空关键词直接回弹且不改变焦点。
+25 -4
View File
@@ -61,6 +61,14 @@ class DiscoverLogic extends ChangeNotifier {
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() {
_searchTimer?.cancel();
_searchRequest++;
@@ -85,7 +93,7 @@ class DiscoverLogic extends ChangeNotifier {
_searchTimer = Timer(searchDebounce, () => unawaited(_runSearch(query)));
}
Future<void> _runSearch(String query) async {
Future<void> _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 [];
}
+49 -17
View File
@@ -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<SearchPage> {
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<SearchPage> {
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<SearchPage> {
);
}
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<SearchPage> {
AppDimens.searchResultTextExtent;
return GridView.builder(
key: const Key('search-results-grid'),
physics: AppPullToRefresh.physics,
padding: const EdgeInsets.fromLTRB(
AppDimens.feedPageHorizontal,
AppDimens.spaceMd,