feat(client): 列表屏搜索前置/显示字段记忆/移动端表格切换与工具栏瘦身/退出登录归位

- 库存查询搜索框移到工具栏最前(桌面)/独占首行(移动端)
- 修复入库/出库/财务「显示字段」失效:移除 minWidth 运行时强制隐藏,
  用户勾选权威、minWidth 仅作首次默认;新增 ColumnPrefs 持久化记忆(四屏独立)
- 移动端库存支持表格/卡片列表切换
- 移动端三个列表屏工具栏改 Wrap 紧凑布局、导出/显示字段图标化
- 退出登录统一到左侧导航:删系统设置内与桌面右上角下拉,桌面左侧栏底部新增

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-17 18:33:31 +08:00
parent 48e282dd97
commit dfa1920dda
8 changed files with 478 additions and 204 deletions
@@ -14,6 +14,7 @@ import '../../providers/inventory_provider.dart';
import '../../widgets/data_table_card.dart';
import '../../widgets/mobile_list_card.dart';
import '../../widgets/multi_select_dropdown.dart' show ColDef, ColumnToggleButton, FilterableColumnHeader;
import '../../core/storage/column_prefs.dart';
import '../../widgets/page_scaffold.dart';
import '../../core/utils/export_util.dart';
import '../../core/utils/print_util.dart';
@@ -37,7 +38,10 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
Set<String> _filterWarehouse = {};
Set<String> _filterSpec = {};
Set<String> _filterSeries = {};
Set<String> _hiddenCols = {};
Set<String>? _hiddenCols; // null = 尚未载入本地存档
bool _mobileTableView = false; // 移动端:false=卡片列表,true=表格(横向滚动)
static const _screenId = 'inventory_list';
static const _colDefs = [
ColDef('code', '商品编码', required: true),
@@ -57,6 +61,14 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
];
@override
void initState() {
super.initState();
ColumnPrefs.load(_screenId).then((saved) {
if (saved != null && mounted) setState(() => _hiddenCols = saved);
});
}
@override
void dispose() {
_searchCtrl.dispose();
@@ -499,8 +511,9 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
? items
: items.where((i) => _filterWarehouse.contains(i.warehouseName)).toList();
final hidden = _hiddenCols ?? <String>{};
final visibleCols =
_colDefs.where((c) => !_hiddenCols.contains(c.key)).toList();
_colDefs.where((c) => !hidden.contains(c.key)).toList();
return Column(
children: [
@@ -553,18 +566,9 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
ref.read(inventoryListProvider.notifier).setPage(p),
onPageSizeChanged: (s) =>
ref.read(inventoryListProvider.notifier).setPageSize(s),
toolbar: Row(
children: [
if (!WriteGuard.isReadonly(ref)) ...[
OutlinedButton.icon(
onPressed: () => context.go('/inventory/check'),
icon: const Icon(Icons.fact_check, size: 16),
label: const Text('发起盘点'),
),
const SizedBox(width: 8),
],
OutlinedButton.icon(
onPressed: () => exportExcel(
toolbar: Builder(builder: (ctx) {
final isMobile = ctx.isMobile;
void doExport() => exportExcel(
filename: '库存查询',
headers: ['商品编码', '商品名称', '规格', '系列', '批次号', '仓库', '库存量', '单价', '生产日期', '入库时间', '供应商', '安全库存', '状态'],
rows: filteredItems.map((item) {
@@ -589,38 +593,112 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
status,
];
}).toList(),
);
final searchField = TextField(
controller: _searchCtrl,
decoration: InputDecoration(
hintText: '名称/编码/拼音,回车搜索',
prefixIcon: const Icon(Icons.search, size: 16),
hintStyle: const TextStyle(fontSize: 12),
suffixIcon: IconButton(
icon: const Icon(Icons.search, size: 16),
tooltip: '搜索',
onPressed: _triggerSearch,
),
icon: const Icon(Icons.download, size: 16),
label: const Text('导出'),
),
const SizedBox(width: 8),
ColumnToggleButton(
columns: _colDefs,
hidden: _hiddenCols,
onChanged: (v) => setState(() => _hiddenCols = v),
),
const Spacer(),
SizedBox(
width: 220,
child: TextField(
controller: _searchCtrl,
decoration: InputDecoration(
hintText: '名称/编码/拼音,回车搜索',
prefixIcon: const Icon(Icons.search, size: 16),
hintStyle: const TextStyle(fontSize: 12),
suffixIcon: IconButton(
icon: const Icon(Icons.search, size: 16),
tooltip: '搜索',
onPressed: _triggerSearch,
),
onSubmitted: (_) => _triggerSearch(),
);
final canCheck = !WriteGuard.isReadonly(ref);
if (isMobile) {
// 移动端:搜索独占一行 + 紧凑图标操作行(含 表格/列表 切换)
return Column(
mainAxisSize: MainAxisSize.min,
children: [
searchField,
const SizedBox(height: 8),
Row(
children: [
if (canCheck)
OutlinedButton.icon(
onPressed: () => context.go('/inventory/check'),
icon: const Icon(Icons.fact_check, size: 16),
label: const Text('盘点'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6),
minimumSize: Size.zero,
tapTargetSize:
MaterialTapTargetSize.shrinkWrap,
),
),
const Spacer(),
IconButton(
tooltip: _mobileTableView ? '卡片视图' : '表格视图',
icon: Icon(
_mobileTableView
? Icons.view_agenda_outlined
: Icons.table_rows_outlined,
size: 20),
onPressed: () => setState(
() => _mobileTableView = !_mobileTableView),
),
IconButton(
tooltip: '导出',
icon: const Icon(Icons.download, size: 20),
onPressed: doExport,
),
ColumnToggleButton(
columns: _colDefs,
hidden: hidden,
compact: true,
onChanged: (v) {
setState(() => _hiddenCols = v);
ColumnPrefs.save(_screenId, v);
},
),
],
),
onSubmitted: (_) => _triggerSearch(),
],
);
}
// 桌面:搜索置于最前,按钮成组靠左
return Row(
children: [
SizedBox(width: 220, child: searchField),
const SizedBox(width: 12),
if (canCheck) ...[
OutlinedButton.icon(
onPressed: () => context.go('/inventory/check'),
icon: const Icon(Icons.fact_check, size: 16),
label: const Text('发起盘点'),
),
const SizedBox(width: 8),
],
OutlinedButton.icon(
onPressed: doExport,
icon: const Icon(Icons.download, size: 16),
label: const Text('导出'),
),
),
],
),
mobileCards:
filteredItems.map(_inventoryCard).toList(),
const SizedBox(width: 8),
ColumnToggleButton(
columns: _colDefs,
hidden: hidden,
onChanged: (v) {
setState(() => _hiddenCols = v);
ColumnPrefs.save(_screenId, v);
},
),
const Spacer(),
],
);
}),
mobileCards: _mobileTableView
? null
: filteredItems.map(_inventoryCard).toList(),
columns: visibleCols.map((c) {
final label = switch (c.key) {
'spec' => FilterableColumnHeader(