feat: 增加搜索结果下拉刷新
This commit is contained in:
1 parent
281f13a810
commit
6b3d870c93
3 files changed
+75
-21
No files matched your search
@@ -1311,3 +1311,4 @@ $ flutter test
|
||||
- 搜索页提供返回、自动聚焦输入框与取消,复用 `/app/search` 逻辑和首页三列卡片。
|
||||
- 清理旧的 searchRequest/focusSearch/syncSearch 参数;`flutter analyze`:No issues found。
|
||||
- 搜索页结果改为独立两列卡片,左右 14、列距 12、行距 16,并移除 HomeFeedCard 复用。
|
||||
- 搜索结果支持下拉刷新:保留旧结果按当前关键词重取第一页,空关键词直接回弹且不改变焦点。
|
||||
@@ -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 [];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user