feat(client): 入库/出库搜索框支持酒名,并对搜索限流

- 搜索框 hint 改为「单号/往来单位/酒名」
- setKeyword 加去重+350ms 防抖:同词不重查、连发回车/狂点合并取最后一次,
  避免酒名模糊查询(单次约 55ms 明细扫描)被刷
- Timer 在 provider dispose 时取消

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-23 23:57:28 +08:00
parent e8f416ddea
commit 102ace42ac
4 changed files with 30 additions and 8 deletions
+14 -3
View File
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/api/api_client.dart'; import '../core/api/api_client.dart';
import '../core/auth/auth_state.dart'; import '../core/auth/auth_state.dart';
@@ -25,9 +27,11 @@ class StockInListNotifier extends AsyncNotifier<PageResult<StockInOrder>> {
String? _endDate; String? _endDate;
String _keyword = ''; String _keyword = '';
PageResult<StockInOrder>? _cache; PageResult<StockInOrder>? _cache;
Timer? _searchDebounce;
@override @override
Future<PageResult<StockInOrder>> build() async { Future<PageResult<StockInOrder>> build() async {
ref.onDispose(() => _searchDebounce?.cancel());
ref.watch(authStateProvider.select((s) => s.user?.shopId)); ref.watch(authStateProvider.select((s) => s.user?.shopId));
ref.watch(networkRecoveryCountProvider); ref.watch(networkRecoveryCountProvider);
try { try {
@@ -78,10 +82,17 @@ class StockInListNotifier extends AsyncNotifier<PageResult<StockInOrder>> {
reload(); reload();
} }
/// 关键词搜索(含酒名模糊反查,单次约 55ms 的明细扫描)。
/// 去重 + 防抖限流:同词不重查;连发回车/狂点合并取最后一次,避免昂贵查询被刷。
void setKeyword(String keyword) { void setKeyword(String keyword) {
_keyword = keyword; final kw = keyword.trim();
_page = 1; if (kw == _keyword) return;
reload(); _searchDebounce?.cancel();
_searchDebounce = Timer(const Duration(milliseconds: 350), () {
_keyword = kw;
_page = 1;
reload();
});
} }
void reload() { void reload() {
+14 -3
View File
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/api/api_client.dart'; import '../core/api/api_client.dart';
import '../core/auth/auth_state.dart'; import '../core/auth/auth_state.dart';
@@ -26,9 +28,11 @@ class StockOutListNotifier extends AsyncNotifier<PageResult<StockOutOrder>> {
String _keyword = ''; String _keyword = '';
String _productCode = ''; String _productCode = '';
PageResult<StockOutOrder>? _cache; PageResult<StockOutOrder>? _cache;
Timer? _searchDebounce;
@override @override
Future<PageResult<StockOutOrder>> build() async { Future<PageResult<StockOutOrder>> build() async {
ref.onDispose(() => _searchDebounce?.cancel());
ref.watch(authStateProvider.select((s) => s.user?.shopId)); ref.watch(authStateProvider.select((s) => s.user?.shopId));
ref.watch(networkRecoveryCountProvider); ref.watch(networkRecoveryCountProvider);
try { try {
@@ -80,10 +84,17 @@ class StockOutListNotifier extends AsyncNotifier<PageResult<StockOutOrder>> {
reload(); reload();
} }
/// 关键词搜索(含酒名模糊反查,单次约 55ms 的明细扫描)。
/// 去重 + 防抖限流:同词不重查;连发回车/狂点合并取最后一次,避免昂贵查询被刷。
void setKeyword(String keyword) { void setKeyword(String keyword) {
_keyword = keyword; final kw = keyword.trim();
_page = 1; if (kw == _keyword) return;
reload(); _searchDebounce?.cancel();
_searchDebounce = Timer(const Duration(milliseconds: 350), () {
_keyword = kw;
_page = 1;
reload();
});
} }
/// 按商品编码精确反查(独立于 keyword) /// 按商品编码精确反查(独立于 keyword)
@@ -366,7 +366,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
final searchField = TextField( final searchField = TextField(
controller: _searchCtrl, controller: _searchCtrl,
decoration: InputDecoration( decoration: InputDecoration(
hintText: '单号/往来单位,回车搜索', hintText: '单号/往来单位/酒名,回车搜索',
prefixIcon: const Icon(Icons.search, size: 16), prefixIcon: const Icon(Icons.search, size: 16),
hintStyle: const TextStyle(fontSize: 12), hintStyle: const TextStyle(fontSize: 12),
suffixIcon: IconButton( suffixIcon: IconButton(
@@ -378,7 +378,7 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
final searchField = TextField( final searchField = TextField(
controller: _searchCtrl, controller: _searchCtrl,
decoration: InputDecoration( decoration: InputDecoration(
hintText: '单号/往来单位,回车搜索', hintText: '单号/往来单位/酒名,回车搜索',
prefixIcon: const Icon(Icons.search, size: 16), prefixIcon: const Icon(Icons.search, size: 16),
hintStyle: const TextStyle(fontSize: 12), hintStyle: const TextStyle(fontSize: 12),
suffixIcon: IconButton( suffixIcon: IconButton(