From a678c3806e265d29fd1363e11a26d8535e8ef378 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Wed, 24 Jun 2026 12:08:55 +0800 Subject: [PATCH] chore: release client-v1.0.79 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 入库/出库单搜索框升级:搜索后关键词转为框内 chip 标签 + 一键清除(✕),仅保留单个搜索词;新增 SearchChip 组件 + provider currentKeyword getter 进屏同步。 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01YZ4DskSRKsSiheQonFtQvx --- CHANGELOG-client.md | 5 ++ client/lib/providers/stock_in_provider.dart | 2 + client/lib/providers/stock_out_provider.dart | 2 + .../stock_in/stock_in_list_screen.dart | 42 +++++++++++++-- .../stock_out/stock_out_list_screen.dart | 43 +++++++++++++-- client/lib/widgets/search_chip.dart | 52 +++++++++++++++++++ 6 files changed, 137 insertions(+), 9 deletions(-) create mode 100644 client/lib/widgets/search_chip.dart diff --git a/CHANGELOG-client.md b/CHANGELOG-client.md index 3597e07..0c785dc 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.79] - 2026-06-24 + +### 改进 +- 入库单 / 出库单搜索框升级:搜索后关键词以标签形式显示在搜索框内,带一键清除(✕)按钮,输入框自动清空便于继续输入;当前仅支持单个搜索词。 + ## [1.0.78] - 2026-06-24 ### 改进 diff --git a/client/lib/providers/stock_in_provider.dart b/client/lib/providers/stock_in_provider.dart index 31a3992..1f9383c 100644 --- a/client/lib/providers/stock_in_provider.dart +++ b/client/lib/providers/stock_in_provider.dart @@ -69,6 +69,8 @@ class StockInListNotifier extends AsyncNotifier> { /// 当前服务端状态过滤值(供页面进入时对齐标签/下拉,避免重复拉取) String get currentStatus => _status; + String get currentKeyword => _keyword; + void setStatus(String status) { _status = status; _page = 1; diff --git a/client/lib/providers/stock_out_provider.dart b/client/lib/providers/stock_out_provider.dart index 1c21a2b..a1668c4 100644 --- a/client/lib/providers/stock_out_provider.dart +++ b/client/lib/providers/stock_out_provider.dart @@ -71,6 +71,8 @@ class StockOutListNotifier extends AsyncNotifier> { /// 当前服务端状态过滤值(供页面进入时对齐标签/下拉,避免重复拉取) String get currentStatus => _status; + String get currentKeyword => _keyword; + void setStatus(String status) { _status = status; _page = 1; diff --git a/client/lib/screens/stock_in/stock_in_list_screen.dart b/client/lib/screens/stock_in/stock_in_list_screen.dart index 103f68b..a168c2f 100644 --- a/client/lib/screens/stock_in/stock_in_list_screen.dart +++ b/client/lib/screens/stock_in/stock_in_list_screen.dart @@ -17,6 +17,7 @@ import '../../widgets/multi_select_dropdown.dart' show ColDef, ColumnToggleButto import '../../core/storage/column_prefs.dart'; import '../../widgets/page_scaffold.dart'; import '../../widgets/status_badge.dart'; +import '../../widgets/search_chip.dart'; import '../../core/utils/export_util.dart'; import '../../providers/inventory_provider.dart'; import '../../providers/tab_state_provider.dart'; @@ -44,6 +45,7 @@ class _StockInListScreenState extends ConsumerState { Set _filterSupplier = {}; Set? _hiddenCols; // null = 尚未载入本地存档(回退到 minWidth 首次默认) final _searchCtrl = TextEditingController(); + String _appliedKw = ''; // 已生效的搜索词(框内 chip 标签展示) static const _screenId = 'stock_in_list'; @@ -72,6 +74,10 @@ class _StockInListScreenState extends ConsumerState { final want = tab == 1 ? 'pending,draft' : _statusFilter; final notifier = ref.read(stockInListProvider.notifier); if (notifier.currentStatus != want) notifier.setStatus(want); + // 同步已生效的搜索词到框内 chip(provider 持久化,切走再回来不丢) + if (notifier.currentKeyword != _appliedKw) { + setState(() => _appliedKw = notifier.currentKeyword); + } }); } @@ -81,8 +87,19 @@ class _StockInListScreenState extends ConsumerState { super.dispose(); } - void _triggerSearch() => - ref.read(stockInListProvider.notifier).setKeyword(_searchCtrl.text.trim()); + void _triggerSearch() { + final kw = _searchCtrl.text.trim(); + ref.read(stockInListProvider.notifier).setKeyword(kw); + // 仅支持单个搜索词:生效后输入框清空,搜索词转为框内 chip 标签 + setState(() => _appliedKw = kw); + if (kw.isNotEmpty) _searchCtrl.clear(); + } + + void _clearSearch() { + _searchCtrl.clear(); + setState(() => _appliedKw = ''); + ref.read(stockInListProvider.notifier).setKeyword(''); + } String? get _startDate => _dateRange != null ? '${_dateRange!.start.year}-${_dateRange!.start.month.toString().padLeft(2, '0')}-${_dateRange!.start.day.toString().padLeft(2, '0')}' @@ -363,12 +380,29 @@ class _StockInListScreenState extends ConsumerState { ) : null; + final hasKw = _appliedKw.isNotEmpty; final searchField = TextField( controller: _searchCtrl, decoration: InputDecoration( - hintText: '单号/往来单位/酒名,回车搜索', - prefixIcon: const Icon(Icons.search, size: 16), + hintText: hasKw ? '继续输入新关键词…' : '单号/往来单位/酒名,回车搜索', hintStyle: const TextStyle(fontSize: 12), + // 搜索图标 +(已生效时)框内 chip 标签,仅一个词;✕ 删除即清除搜索。 + // 用 prefixIcon 而非 prefix:后者仅在聚焦/有文字时才显示,搜索后输入框为空会被隐藏。 + prefixIconConstraints: + const BoxConstraints(minWidth: 0, minHeight: 0), + prefixIcon: Padding( + padding: const EdgeInsets.only(left: 8, right: 6), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.search, size: 16), + if (hasKw) ...[ + const SizedBox(width: 6), + SearchChip(label: _appliedKw, onDeleted: _clearSearch), + ], + ], + ), + ), suffixIcon: IconButton( icon: const Icon(Icons.search, size: 16), tooltip: '搜索', diff --git a/client/lib/screens/stock_out/stock_out_list_screen.dart b/client/lib/screens/stock_out/stock_out_list_screen.dart index 7f619b9..28f149a 100644 --- a/client/lib/screens/stock_out/stock_out_list_screen.dart +++ b/client/lib/screens/stock_out/stock_out_list_screen.dart @@ -15,6 +15,7 @@ import '../../widgets/multi_select_dropdown.dart' show ColDef, ColumnToggleButto import '../../core/storage/column_prefs.dart'; import '../../widgets/page_scaffold.dart'; import '../../widgets/status_badge.dart'; +import '../../widgets/search_chip.dart'; import '../../core/utils/export_util.dart'; import '../../core/utils/print_util.dart'; import '../../providers/inventory_provider.dart'; @@ -43,6 +44,7 @@ class _StockOutListScreenState extends ConsumerState { Set? _hiddenCols; // null = 尚未载入本地存档(回退到 minWidth 首次默认) final _searchCtrl = TextEditingController(); final _codeSearchCtrl = TextEditingController(); + String _appliedKw = ''; // 已生效的搜索词(框内 chip 标签展示) static const _screenId = 'stock_out_list'; @@ -59,6 +61,10 @@ class _StockOutListScreenState extends ConsumerState { final want = tab == 1 ? 'pending,draft' : _statusFilter; final notifier = ref.read(stockOutListProvider.notifier); if (notifier.currentStatus != want) notifier.setStatus(want); + // 同步已生效的搜索词到框内 chip(provider 持久化,切走再回来不丢) + if (notifier.currentKeyword != _appliedKw) { + setState(() => _appliedKw = notifier.currentKeyword); + } }); } @@ -82,9 +88,19 @@ class _StockOutListScreenState extends ConsumerState { super.dispose(); } - void _triggerSearch() => ref - .read(stockOutListProvider.notifier) - .setKeyword(_searchCtrl.text.trim()); + void _triggerSearch() { + final kw = _searchCtrl.text.trim(); + ref.read(stockOutListProvider.notifier).setKeyword(kw); + // 仅支持单个搜索词:生效后输入框清空,搜索词转为框内 chip 标签 + setState(() => _appliedKw = kw); + if (kw.isNotEmpty) _searchCtrl.clear(); + } + + void _clearSearch() { + _searchCtrl.clear(); + setState(() => _appliedKw = ''); + ref.read(stockOutListProvider.notifier).setKeyword(''); + } void _triggerCodeSearch() => ref .read(stockOutListProvider.notifier) @@ -375,12 +391,29 @@ class _StockOutListScreenState extends ConsumerState { ) : null; + final hasKw = _appliedKw.isNotEmpty; final searchField = TextField( controller: _searchCtrl, decoration: InputDecoration( - hintText: '单号/往来单位/酒名,回车搜索', - prefixIcon: const Icon(Icons.search, size: 16), + hintText: hasKw ? '继续输入新关键词…' : '单号/往来单位/酒名,回车搜索', hintStyle: const TextStyle(fontSize: 12), + // 搜索图标 +(已生效时)框内 chip 标签,仅一个词;✕ 删除即清除搜索。 + // 用 prefixIcon 而非 prefix:后者仅在聚焦/有文字时才显示,搜索后输入框为空会被隐藏。 + prefixIconConstraints: + const BoxConstraints(minWidth: 0, minHeight: 0), + prefixIcon: Padding( + padding: const EdgeInsets.only(left: 8, right: 6), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.search, size: 16), + if (hasKw) ...[ + const SizedBox(width: 6), + SearchChip(label: _appliedKw, onDeleted: _clearSearch), + ], + ], + ), + ), suffixIcon: IconButton( icon: const Icon(Icons.search, size: 16), tooltip: '搜索', diff --git a/client/lib/widgets/search_chip.dart b/client/lib/widgets/search_chip.dart new file mode 100644 index 0000000..51262c9 --- /dev/null +++ b/client/lib/widgets/search_chip.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +/// 搜索框内嵌的「已生效搜索词」标签:紧凑圆角 chip + ✕ 删除按钮。 +/// +/// 用于列表屏搜索框 [InputDecoration.prefix],仅展示单个搜索词; +/// 点 ✕ 触发 [onDeleted] 清除搜索。 +class SearchChip extends StatelessWidget { + const SearchChip({super.key, required this.label, required this.onDeleted}); + + final String label; + final VoidCallback onDeleted; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final bg = theme.colorScheme.primary.withValues(alpha: 0.12); + final fg = theme.colorScheme.primary; + return Container( + padding: const EdgeInsets.only(left: 8, right: 2, top: 2, bottom: 2), + decoration: BoxDecoration( + color: bg, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 120), + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12, + color: fg, + fontWeight: FontWeight.w500, + ), + ), + ), + InkResponse( + onTap: onDeleted, + radius: 14, + child: Padding( + padding: const EdgeInsets.all(2), + child: Icon(Icons.close, size: 14, color: fg), + ), + ), + ], + ), + ); + } +}