diff --git a/client/lib/screens/finance/finance_screen.dart b/client/lib/screens/finance/finance_screen.dart index c64513c..c57cf05 100644 --- a/client/lib/screens/finance/finance_screen.dart +++ b/client/lib/screens/finance/finance_screen.dart @@ -4,7 +4,7 @@ import '../../core/theme/app_theme.dart'; import '../../models/finance.dart'; import '../../providers/finance_provider.dart'; import '../../widgets/data_table_card.dart'; -import '../../widgets/multi_select_dropdown.dart'; +import '../../widgets/multi_select_dropdown.dart' show ColDef, ColumnToggleButton, FilterableColumnHeader; import '../../widgets/page_scaffold.dart'; class FinanceScreen extends ConsumerWidget { @@ -161,12 +161,26 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> { (c.minWidth == null || screenWidth >= c.minWidth!)) .toList(); - final columns = visibleCols - .map((c) => DataColumn( - label: Text(c.label), - numeric: c.key == 'amount' || c.key == 'balance', - )) - .toList(); + final columns = visibleCols.map((c) { + final label = switch (c.key) { + 'type' => FilterableColumnHeader( + text: c.label, + options: typeOptions, + selected: _filterType, + onChanged: (v) => setState(() => _filterType = v), + ), + 'partner' => FilterableColumnHeader( + text: c.label, + options: partnerOptions, + selected: _filterPartner, + onChanged: (v) => setState(() => _filterPartner = v), + ), + _ => Text(c.label), + }; + return DataColumn( + label: label, + numeric: c.key == 'amount' || c.key == 'balance'); + }).toList(); DataCell buildFinanceCell(String key, FinanceRecord r) { switch (key) { @@ -284,44 +298,22 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> { _fetch(); }); }, - toolbar: Column( - crossAxisAlignment: CrossAxisAlignment.start, + toolbar: Row( children: [ - // Filter bar + month selector + column toggle - Row( - children: [ - if (typeOptions.length > 1) - MultiSelectDropdown( - label: '类型', - options: typeOptions, - selected: _filterType, - onChanged: (v) => setState(() => _filterType = v), - ), - if (typeOptions.length > 1) const SizedBox(width: 8), - if (partnerOptions.length > 1) - MultiSelectDropdown( - label: '往来单位', - options: partnerOptions, - selected: _filterPartner, - onChanged: (v) => - setState(() => _filterPartner = v), - ), - const Spacer(), - _MonthSelector( - value: _month, - onChanged: (v) { - _month = v; - _page = 1; - _refetch(); - }, - ), - const SizedBox(width: 8), - ColumnToggleButton( - columns: _colDefs, - hidden: _hiddenCols, - onChanged: (v) => setState(() => _hiddenCols = v), - ), - ], + const Spacer(), + _MonthSelector( + value: _month, + onChanged: (v) { + _month = v; + _page = 1; + _refetch(); + }, + ), + const SizedBox(width: 8), + ColumnToggleButton( + columns: _colDefs, + hidden: _hiddenCols, + onChanged: (v) => setState(() => _hiddenCols = v), ), ], ), diff --git a/client/lib/screens/inventory/batch_tracking_screen.dart b/client/lib/screens/inventory/batch_tracking_screen.dart index 00b8aba..483df92 100644 --- a/client/lib/screens/inventory/batch_tracking_screen.dart +++ b/client/lib/screens/inventory/batch_tracking_screen.dart @@ -4,7 +4,7 @@ import '../../core/theme/app_theme.dart'; import '../../models/inventory.dart'; import '../../providers/inventory_provider.dart'; import '../../widgets/data_table_card.dart'; -import '../../widgets/multi_select_dropdown.dart'; +import '../../widgets/multi_select_dropdown.dart' show ColDef, ColumnToggleButton, FilterableColumnHeader; class BatchTrackingScreen extends ConsumerStatefulWidget { const BatchTrackingScreen({super.key}); @@ -135,12 +135,32 @@ class _BatchTrackingScreenState extends ConsumerState { (c.minWidth == null || screenWidth >= c.minWidth!)) .toList(); - final columns = visibleCols - .map((c) => DataColumn( - label: Text(c.label), - numeric: c.key == 'qty' || c.key == 'price', - )) - .toList(); + final columns = visibleCols.map((c) { + final label = switch (c.key) { + 'status' => FilterableColumnHeader( + text: c.label, + options: const ['在售', '已卖出'], + selected: _filterStatus, + onChanged: (v) => setState(() => _filterStatus = v), + ), + 'warehouse' => FilterableColumnHeader( + text: c.label, + options: warehouseOptions, + selected: _filterWarehouse, + onChanged: (v) => setState(() => _filterWarehouse = v), + ), + 'supplier' => FilterableColumnHeader( + text: c.label, + options: supplierOptions, + selected: _filterSupplier, + onChanged: (v) => setState(() => _filterSupplier = v), + ), + _ => Text(c.label), + }; + return DataColumn( + label: label, + numeric: c.key == 'qty' || c.key == 'price'); + }).toList(); final rows = records.isEmpty ? [ @@ -169,59 +189,24 @@ class _BatchTrackingScreenState extends ConsumerState { _page = p; _fetch(); }), - toolbar: Column( - crossAxisAlignment: CrossAxisAlignment.start, + toolbar: Row( children: [ - // Top row: description + refresh + column toggle - Row( - children: [ - const Text('已审核入库商品(含库存与销售状态)', - style: TextStyle( - fontSize: 13, color: AppTheme.textSecondary)), - const Spacer(), - IconButton( - icon: const Icon(Icons.refresh, size: 18), - onPressed: () => setState(() { - _page = 1; - _fetch(); - }), - tooltip: '刷新', - ), - const SizedBox(width: 4), - ColumnToggleButton( - columns: _colDefs, - hidden: _hiddenCols, - onChanged: (v) => setState(() => _hiddenCols = v), - ), - ], + const Text('已审核入库商品(含库存与销售状态)', + style: TextStyle(fontSize: 13, color: AppTheme.textSecondary)), + const Spacer(), + IconButton( + icon: const Icon(Icons.refresh, size: 18), + onPressed: () => setState(() { + _page = 1; + _fetch(); + }), + tooltip: '刷新', ), - // Filter bar row - const SizedBox(height: 6), - Row( - children: [ - MultiSelectDropdown( - label: '状态', - options: const ['在售', '已卖出'], - selected: _filterStatus, - onChanged: (v) => setState(() => _filterStatus = v), - ), - const SizedBox(width: 8), - if (warehouseOptions.length > 1) - MultiSelectDropdown( - label: '仓库', - options: warehouseOptions, - selected: _filterWarehouse, - onChanged: (v) => setState(() => _filterWarehouse = v), - ), - if (warehouseOptions.length > 1) const SizedBox(width: 8), - if (supplierOptions.length > 1) - MultiSelectDropdown( - label: '供应商', - options: supplierOptions, - selected: _filterSupplier, - onChanged: (v) => setState(() => _filterSupplier = v), - ), - ], + const SizedBox(width: 4), + ColumnToggleButton( + columns: _colDefs, + hidden: _hiddenCols, + onChanged: (v) => setState(() => _hiddenCols = v), ), ], ), diff --git a/client/lib/screens/inventory/inventory_list_screen.dart b/client/lib/screens/inventory/inventory_list_screen.dart index 47de14b..8529f13 100644 --- a/client/lib/screens/inventory/inventory_list_screen.dart +++ b/client/lib/screens/inventory/inventory_list_screen.dart @@ -5,8 +5,8 @@ import 'package:go_router/go_router.dart'; import '../../core/theme/app_theme.dart'; import '../../models/inventory.dart'; import '../../providers/inventory_provider.dart'; -import '../../providers/warehouse_provider.dart'; import '../../widgets/data_table_card.dart'; +import '../../widgets/multi_select_dropdown.dart' show FilterableColumnHeader; import '../../widgets/page_scaffold.dart'; class InventoryListScreen extends ConsumerStatefulWidget { @@ -20,7 +20,7 @@ class InventoryListScreen extends ConsumerStatefulWidget { class _InventoryListScreenState extends ConsumerState { final _searchCtrl = TextEditingController(); Timer? _debounce; - int? _warehouseFilter; + Set _filterWarehouse = {}; @override void dispose() { @@ -55,7 +55,6 @@ class _InventoryListScreenState extends ConsumerState { Widget _buildInventoryTab() { final asyncInventory = ref.watch(inventoryListProvider); - final asyncWarehouses = ref.watch(warehouseListProvider); return asyncInventory.when( loading: () => const Center(child: CircularProgressIndicator()), @@ -82,6 +81,19 @@ class _InventoryListScreenState extends ConsumerState { items.where((i) => i.minStock != null && i.quantity < i.minStock!).length; final emptyCount = items.where((i) => i.quantity == 0).length; + // 仓库选项从数据中派生,客户端筛选 + final warehouseOptions = items + .map((i) => i.warehouseName ?? '') + .where((s) => s.isNotEmpty) + .toSet() + .toList() + ..sort(); + final filteredItems = _filterWarehouse.isEmpty + ? items + : items + .where((i) => _filterWarehouse.contains(i.warehouseName ?? '')) + .toList(); + return Column( children: [ // Summary cards @@ -116,7 +128,7 @@ class _InventoryListScreenState extends ConsumerState { const Divider(height: 1), Expanded( child: DataTableCard( - totalCount: result.total, + totalCount: filteredItems.length, page: result.page, onPageChanged: (p) => ref.read(inventoryListProvider.notifier).setPage(p), @@ -128,49 +140,6 @@ class _InventoryListScreenState extends ConsumerState { label: const Text('发起盘点'), ), const Spacer(), - // Warehouse filter - asyncWarehouses.when( - loading: () => const SizedBox(), - error: (_, __) => const SizedBox(), - data: (warehouses) => Container( - height: 36, - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - border: Border.all(color: AppTheme.border), - borderRadius: BorderRadius.circular(4), - color: AppTheme.surface, - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: _warehouseFilter, - hint: const Text('全部仓库', - style: TextStyle(fontSize: 13)), - items: [ - const DropdownMenuItem( - value: null, - child: Text('全部仓库', - style: TextStyle(fontSize: 13))), - ...warehouses.map((w) => - DropdownMenuItem( - value: w.id, - child: Text(w.name, - style: const TextStyle( - fontSize: 13)))), - ], - onChanged: (v) { - setState(() => _warehouseFilter = v); - ref - .read(inventoryListProvider.notifier) - .setWarehouseId(v); - }, - style: const TextStyle( - fontSize: 13, - color: AppTheme.textPrimary), - ), - ), - ), - ), - const SizedBox(width: 8), SizedBox( width: 200, child: TextField( @@ -185,15 +154,22 @@ class _InventoryListScreenState extends ConsumerState { ), ], ), - columns: const [ - DataColumn(label: Text('商品编码')), - DataColumn(label: Text('商品名称')), - DataColumn(label: Text('品牌')), - DataColumn(label: Text('规格')), - DataColumn(label: Text('仓库')), - DataColumn(label: Text('库存'), numeric: true), - DataColumn(label: Text('安全库存'), numeric: true), - DataColumn(label: Text('状态')), + columns: [ + const DataColumn(label: Text('商品编码')), + const DataColumn(label: Text('商品名称')), + const DataColumn(label: Text('品牌')), + const DataColumn(label: Text('规格')), + DataColumn( + label: FilterableColumnHeader( + text: '仓库', + options: warehouseOptions, + selected: _filterWarehouse, + onChanged: (v) => setState(() => _filterWarehouse = v), + ), + ), + const DataColumn(label: Text('库存'), numeric: true), + const DataColumn(label: Text('安全库存'), numeric: true), + const DataColumn(label: Text('状态')), ], rows: items.isEmpty ? [ @@ -210,7 +186,7 @@ class _InventoryListScreenState extends ConsumerState { DataCell(SizedBox()), ]) ] - : items + : filteredItems .map((item) => DataRow( color: WidgetStateProperty.resolveWith( (states) { diff --git a/client/lib/screens/products/products_screen.dart b/client/lib/screens/products/products_screen.dart index 1803ed8..bc227e4 100644 --- a/client/lib/screens/products/products_screen.dart +++ b/client/lib/screens/products/products_screen.dart @@ -5,6 +5,7 @@ import '../../core/theme/app_theme.dart'; import '../../models/product.dart'; import '../../providers/product_provider.dart'; import '../../widgets/data_table_card.dart'; +import '../../widgets/multi_select_dropdown.dart' show FilterableColumnHeader; import '../../widgets/page_scaffold.dart'; class ProductsScreen extends ConsumerStatefulWidget { @@ -17,6 +18,7 @@ class ProductsScreen extends ConsumerStatefulWidget { class _ProductsScreenState extends ConsumerState { final _searchCtrl = TextEditingController(); Timer? _debounce; + Set _filterBrand = {}; @override void dispose() { @@ -133,6 +135,17 @@ class _ProductsScreenState extends ConsumerState { Widget _buildProductList( List products, int totalCount, int page) { + // 品牌选项从当前数据中派生,客户端筛选 + final brandOptions = products + .map((p) => p.brand ?? '') + .where((s) => s.isNotEmpty) + .toSet() + .toList() + ..sort(); + final filteredProducts = _filterBrand.isEmpty + ? products + : products.where((p) => _filterBrand.contains(p.brand ?? '')).toList(); + return DataTableCard( totalCount: totalCount, page: page, @@ -160,18 +173,25 @@ class _ProductsScreenState extends ConsumerState { ), ], ), - columns: const [ - DataColumn(label: Text('商品编码')), - DataColumn(label: Text('商品名称')), - DataColumn(label: Text('品牌')), - DataColumn(label: Text('规格')), - DataColumn(label: Text('单位')), - DataColumn(label: Text('进价'), numeric: true), - DataColumn(label: Text('售价'), numeric: true), - DataColumn(label: Text('安全库存'), numeric: true), - DataColumn(label: Text('操作')), + columns: [ + const DataColumn(label: Text('商品编码')), + const DataColumn(label: Text('商品名称')), + DataColumn( + label: FilterableColumnHeader( + text: '品牌', + options: brandOptions, + selected: _filterBrand, + onChanged: (v) => setState(() => _filterBrand = v), + ), + ), + const DataColumn(label: Text('规格')), + const DataColumn(label: Text('单位')), + const DataColumn(label: Text('进价'), numeric: true), + const DataColumn(label: Text('售价'), numeric: true), + const DataColumn(label: Text('安全库存'), numeric: true), + const DataColumn(label: Text('操作')), ], - rows: products.isEmpty + rows: filteredProducts.isEmpty ? [ const DataRow(cells: [ DataCell(SizedBox()), @@ -186,7 +206,7 @@ class _ProductsScreenState extends ConsumerState { DataCell(SizedBox()), ]) ] - : products + : filteredProducts .map((p) => DataRow( cells: [ DataCell(Text(p.code, diff --git a/client/lib/widgets/multi_select_dropdown.dart b/client/lib/widgets/multi_select_dropdown.dart index 8f6009d..51e0df5 100644 --- a/client/lib/widgets/multi_select_dropdown.dart +++ b/client/lib/widgets/multi_select_dropdown.dart @@ -176,31 +176,37 @@ class _FilterableColumnHeaderState extends State { onEnter: (_) => setState(() => _hovered = true), onExit: (_) => setState(() => _hovered = false), child: Row( - mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(widget.text), - AnimatedOpacity( - opacity: showIcon ? 1.0 : 0.0, - duration: const Duration(milliseconds: 120), - child: InkWell( - onTap: widget.options.isEmpty ? null : () => _show(context), - borderRadius: BorderRadius.circular(4), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 2), - child: Icon( - active ? Icons.filter_alt : Icons.filter_alt_outlined, - size: 13, - color: active ? AppTheme.primary : AppTheme.textSecondary, + Row( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedOpacity( + opacity: showIcon ? 1.0 : 0.0, + duration: const Duration(milliseconds: 120), + child: InkWell( + onTap: widget.options.isEmpty ? null : () => _show(context), + borderRadius: BorderRadius.circular(4), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 3), + child: Icon( + active ? Icons.filter_alt : Icons.filter_alt_outlined, + size: 16, + color: active ? AppTheme.primary : AppTheme.textSecondary, + ), + ), ), ), - ), + if (active) + InkWell( + onTap: () => widget.onChanged({}), + borderRadius: BorderRadius.circular(4), + child: const Icon(Icons.cancel, size: 14, + color: AppTheme.primary), + ), + ], ), - if (active) - InkWell( - onTap: () => widget.onChanged({}), - borderRadius: BorderRadius.circular(4), - child: const Icon(Icons.cancel, size: 12, color: AppTheme.primary), - ), ], ), );