diff --git a/CHANGELOG-client.md b/CHANGELOG-client.md index 2a51fba..64e3f68 100644 --- a/CHANGELOG-client.md +++ b/CHANGELOG-client.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.73] - 2026-06-22 + +### 改进 +- 出库「添加商品」选货弹窗改为分页加载:默认显示前 20 条,底部显示库存总数并支持上一页/下一页翻页;弹窗打开即加载、按编码/名称/系列搜索时即时分页,避免一次拉取过多导致加载缓慢或看不到商品 + ## [1.0.72] - 2026-06-22 ### 新功能 diff --git a/client/lib/screens/stock_out/stock_out_form_screen.dart b/client/lib/screens/stock_out/stock_out_form_screen.dart index 82ad80c..bce35b2 100644 --- a/client/lib/screens/stock_out/stock_out_form_screen.dart +++ b/client/lib/screens/stock_out/stock_out_form_screen.dart @@ -961,6 +961,27 @@ class _InventoryPickerDialogState }; bool _loading = false; Timer? _debounce; + // 服务端分页:默认第 1 页 20 条;_total 为当前筛选下的库存总数 + static const int _pageSize = 20; + int _page = 1; + int _total = 0; + int get _totalPages => _total <= 0 ? 1 : ((_total + _pageSize - 1) ~/ _pageSize); + + @override + void initState() { + super.initState(); + // 打开即拉取第 1 页(默认前 20 条),不依赖调用方预加载,避免默认空白。 + // 用首帧后回调,避开 initState 同步段调用 setState。 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _fetch(); + }); + } + + void _goPage(int p) { + if (p < 1 || p > _totalPages || _loading) return; + setState(() => _page = p); + _fetch(); + } @override void dispose() { @@ -970,7 +991,11 @@ class _InventoryPickerDialogState } void _onSearchChanged(String v) { - setState(() => _search = v); + // 改搜索词回到第 1 页 + setState(() { + _search = v; + _page = 1; + }); _debounce?.cancel(); _debounce = Timer(const Duration(milliseconds: 300), _fetch); } @@ -982,13 +1007,19 @@ class _InventoryPickerDialogState final result = await ref.read(inventoryRepositoryProvider).listInventory( warehouseId: widget.warehouseId, keyword: kw.isEmpty ? null : kw, - pageSize: AppConstants.stockOutPickerPageSize, + page: _page, + pageSize: _pageSize, ); final items = _aggregatePickerItems(result.data); for (final it in items) { _known[it.productId] = it; } - if (mounted) setState(() => _results = items); + if (mounted) { + setState(() { + _results = items; + _total = result.total; + }); + } } catch (_) { // 忽略:保留上次结果 } finally { @@ -1161,6 +1192,39 @@ class _InventoryPickerDialogState }), child: Text(allSelected ? '取消全选' : '全选当前'), ), + const SizedBox(width: 8), + // 库存总数汇总(无搜索=整仓总数,搜索时=匹配总数) + Text( + _loading ? '加载中…' : '共 $_total 项库存商品', + style: const TextStyle( + fontSize: 13, color: AppTheme.textSecondary), + ), + const SizedBox(width: 12), + // 翻页 + IconButton( + icon: const Icon(Icons.chevron_left, size: 20), + onPressed: (_page > 1 && !_loading) + ? () => _goPage(_page - 1) + : null, + tooltip: '上一页', + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + constraints: + const BoxConstraints(minWidth: 32, minHeight: 32), + ), + Text('第 $_page / $_totalPages 页', + style: const TextStyle(fontSize: 13)), + IconButton( + icon: const Icon(Icons.chevron_right, size: 20), + onPressed: (_page < _totalPages && !_loading) + ? () => _goPage(_page + 1) + : null, + tooltip: '下一页', + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + constraints: + const BoxConstraints(minWidth: 32, minHeight: 32), + ), const Spacer(), OutlinedButton( onPressed: () => Navigator.pop(context),