feat(client): 筛选项移入列头,hover 显示

新增 FilterableColumnHeader widget:鼠标悬停列头时显示漏斗图标,
有活跃筛选时图标常驻并变为主色调,点击打开多选弹窗,活跃时右侧
显示一键清除按钮。

入库单/出库单列表:仓库、供应商/客户列头换用 FilterableColumnHeader,
工具栏从两行合并为一行(移除独立筛选按钮行,显示字段按钮移至右侧)。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-04-14 00:57:48 +08:00
parent 998eb716dc
commit c5c74d59ab
3 changed files with 195 additions and 160 deletions
@@ -6,7 +6,7 @@ import '../../models/stock_in.dart';
import '../../providers/stock_in_provider.dart';
import '../../repositories/stock_in_repository.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';
import '../../widgets/status_badge.dart';
@@ -162,12 +162,24 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
(c.minWidth == null || screenWidth >= c.minWidth!))
.toList();
final columns = visibleCols
.map((c) => DataColumn(
label: Text(c.label),
numeric: c.key == 'amount',
))
.toList();
final columns = visibleCols.map((c) {
final label = switch (c.key) {
'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 == 'amount');
}).toList();
DataCell buildOrderCell(String key, StockInOrder o) {
switch (key) {
@@ -271,81 +283,48 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
page: page,
onPageChanged: (p) =>
ref.read(stockInListProvider.notifier).setPage(p),
toolbar: Column(
crossAxisAlignment: CrossAxisAlignment.start,
toolbar: Row(
children: [
// Row 1: new button + status filter + date picker
Row(
children: [
if (showNewButton)
ElevatedButton.icon(
onPressed: () => context.go('/stock-in/new'),
icon: const Icon(Icons.add, size: 16),
label: const Text('新建入库审核单'),
),
const Spacer(),
if (showStatusFilter) ...[
_StatusFilterDropdown(
value: _statusFilter,
onChanged: (v) {
setState(() => _statusFilter = v ?? '');
ref
.read(stockInListProvider.notifier)
.setStatus(v ?? '');
},
),
const SizedBox(width: 8),
],
OutlinedButton.icon(
onPressed: _pickDateRange,
icon: const Icon(Icons.date_range, size: 16),
label: Text(
_dateRange == null
? '选择日期'
: '$_startDate ~ $_endDate',
style: const TextStyle(fontSize: 13),
),
),
if (_dateRange != null) ...[
const SizedBox(width: 4),
IconButton(
icon: const Icon(Icons.clear, size: 16),
onPressed: () {
setState(() => _dateRange = null);
ref
.read(stockInListProvider.notifier)
.setDateRange(null, null);
},
),
],
],
if (showNewButton)
ElevatedButton.icon(
onPressed: () => context.go('/stock-in/new'),
icon: const Icon(Icons.add, size: 16),
label: const Text('新建入库审核单'),
),
const Spacer(),
if (showStatusFilter) ...[
_StatusFilterDropdown(
value: _statusFilter,
onChanged: (v) {
setState(() => _statusFilter = v ?? '');
ref.read(stockInListProvider.notifier).setStatus(v ?? '');
},
),
const SizedBox(width: 8),
],
OutlinedButton.icon(
onPressed: _pickDateRange,
icon: const Icon(Icons.date_range, size: 16),
label: Text(
_dateRange == null ? '选择日期' : '$_startDate ~ $_endDate',
style: const TextStyle(fontSize: 13),
),
),
// Row 2: multi-select filters + column toggle
const SizedBox(height: 6),
Row(
children: [
if (supplierOptions.length > 1)
MultiSelectDropdown(
label: '供应商',
options: supplierOptions,
selected: _filterSupplier,
onChanged: (v) => setState(() => _filterSupplier = v),
),
if (supplierOptions.length > 1) const SizedBox(width: 8),
if (warehouseOptions.length > 1)
MultiSelectDropdown(
label: '仓库',
options: warehouseOptions,
selected: _filterWarehouse,
onChanged: (v) => setState(() => _filterWarehouse = v),
),
const Spacer(),
ColumnToggleButton(
columns: _colDefs,
hidden: _hiddenCols,
onChanged: (v) => setState(() => _hiddenCols = v),
),
],
if (_dateRange != null) ...[
const SizedBox(width: 4),
IconButton(
icon: const Icon(Icons.clear, size: 16),
onPressed: () {
setState(() => _dateRange = null);
ref.read(stockInListProvider.notifier).setDateRange(null, null);
},
),
],
const SizedBox(width: 8),
ColumnToggleButton(
columns: _colDefs,
hidden: _hiddenCols,
onChanged: (v) => setState(() => _hiddenCols = v),
),
],
),
@@ -6,7 +6,7 @@ import '../../models/stock_out.dart';
import '../../providers/stock_out_provider.dart';
import '../../repositories/stock_out_repository.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';
import '../../widgets/status_badge.dart';
@@ -163,12 +163,24 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
(c.minWidth == null || screenWidth >= c.minWidth!))
.toList();
final columns = visibleCols
.map((c) => DataColumn(
label: Text(c.label),
numeric: c.key == 'amount',
))
.toList();
final columns = visibleCols.map((c) {
final label = switch (c.key) {
'warehouse' => FilterableColumnHeader(
text: c.label,
options: warehouseOptions,
selected: _filterWarehouse,
onChanged: (v) => setState(() => _filterWarehouse = v),
),
'customer' => FilterableColumnHeader(
text: c.label,
options: customerOptions,
selected: _filterCustomer,
onChanged: (v) => setState(() => _filterCustomer = v),
),
_ => Text(c.label),
};
return DataColumn(label: label, numeric: c.key == 'amount');
}).toList();
DataCell buildOrderCell(String key, StockOutOrder o) {
switch (key) {
@@ -272,81 +284,48 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
page: page,
onPageChanged: (p) =>
ref.read(stockOutListProvider.notifier).setPage(p),
toolbar: Column(
crossAxisAlignment: CrossAxisAlignment.start,
toolbar: Row(
children: [
// Row 1: new button + status filter + date picker
Row(
children: [
if (showNewButton)
ElevatedButton.icon(
onPressed: () => context.go('/stock-out/new'),
icon: const Icon(Icons.add, size: 16),
label: const Text('新建出库审核单'),
),
const Spacer(),
if (showStatusFilter) ...[
_StatusFilterDropdown(
value: _statusFilter,
onChanged: (v) {
setState(() => _statusFilter = v ?? '');
ref
.read(stockOutListProvider.notifier)
.setStatus(v ?? '');
},
),
const SizedBox(width: 8),
],
OutlinedButton.icon(
onPressed: _pickDateRange,
icon: const Icon(Icons.date_range, size: 16),
label: Text(
_dateRange == null
? '选择日期'
: '$_startDate ~ $_endDate',
style: const TextStyle(fontSize: 13),
),
),
if (_dateRange != null) ...[
const SizedBox(width: 4),
IconButton(
icon: const Icon(Icons.clear, size: 16),
onPressed: () {
setState(() => _dateRange = null);
ref
.read(stockOutListProvider.notifier)
.setDateRange(null, null);
},
),
],
],
if (showNewButton)
ElevatedButton.icon(
onPressed: () => context.go('/stock-out/new'),
icon: const Icon(Icons.add, size: 16),
label: const Text('新建出库审核单'),
),
const Spacer(),
if (showStatusFilter) ...[
_StatusFilterDropdown(
value: _statusFilter,
onChanged: (v) {
setState(() => _statusFilter = v ?? '');
ref.read(stockOutListProvider.notifier).setStatus(v ?? '');
},
),
const SizedBox(width: 8),
],
OutlinedButton.icon(
onPressed: _pickDateRange,
icon: const Icon(Icons.date_range, size: 16),
label: Text(
_dateRange == null ? '选择日期' : '$_startDate ~ $_endDate',
style: const TextStyle(fontSize: 13),
),
),
// Row 2: multi-select filters + column toggle
const SizedBox(height: 6),
Row(
children: [
if (customerOptions.length > 1)
MultiSelectDropdown(
label: '客户',
options: customerOptions,
selected: _filterCustomer,
onChanged: (v) => setState(() => _filterCustomer = v),
),
if (customerOptions.length > 1) const SizedBox(width: 8),
if (warehouseOptions.length > 1)
MultiSelectDropdown(
label: '仓库',
options: warehouseOptions,
selected: _filterWarehouse,
onChanged: (v) => setState(() => _filterWarehouse = v),
),
const Spacer(),
ColumnToggleButton(
columns: _colDefs,
hidden: _hiddenCols,
onChanged: (v) => setState(() => _hiddenCols = v),
),
],
if (_dateRange != null) ...[
const SizedBox(width: 4),
IconButton(
icon: const Icon(Icons.clear, size: 16),
onPressed: () {
setState(() => _dateRange = null);
ref.read(stockOutListProvider.notifier).setDateRange(null, null);
},
),
],
const SizedBox(width: 8),
ColumnToggleButton(
columns: _colDefs,
hidden: _hiddenCols,
onChanged: (v) => setState(() => _hiddenCols = v),
),
],
),