diff --git a/.superpowers/prototype/screens/inventory.html b/.superpowers/prototype/screens/inventory.html
index 0a932a3..81aa992 100644
--- a/.superpowers/prototype/screens/inventory.html
+++ b/.superpowers/prototype/screens/inventory.html
@@ -59,6 +59,10 @@
+
+
规格
+
系列
+
仓库
状态
重置
@@ -113,16 +117,17 @@ const ITEMS=[
{name:'水井坊 井台',code:'SJF-JT-500',spec:'500ml×6',series:'井台',batch:'PZ231209',cat:'浓香',qty:6,unit:'件',cost:'¥680',price:'¥4,080',prodDate:'2023-12-09',inDate:'2026-05-20',supplier:'四川水井坊',value:'¥4,100',status:'已售'},
];
const STATUS=['全部','在售','库存','已售'];
-const COLS=[{key:'product',label:'商品',fixed:true},{key:'spec',label:'规格'},{key:'series',label:'系列'},{key:'batch',label:'批次号'},{key:'qty',label:'库存',num:true,sort:true},{key:'cost',label:'成本价',num:true,sort:true},{key:'price',label:'总价',num:true,sort:true},{key:'prodDate',label:'生产日期',sort:true},{key:'inDate',label:'入库时间',sort:true},{key:'supplier',label:'供应商'},{key:'status',label:'状态',filter:'status'},{key:'act',label:'操作',fixed:true}];
+const COLS=[{key:'product',label:'商品',fixed:true},{key:'spec',label:'规格'},{key:'series',label:'系列'},{key:'batch',label:'批次号'},{key:'qty',label:'库存',num:true,sort:true},{key:'cost',label:'成本价',num:true,sort:true},{key:'price',label:'总价',num:true,sort:true},{key:'prodDate',label:'生产日期',sort:true},{key:'inDate',label:'入库时间',sort:true},{key:'supplier',label:'供应商'},{key:'status',label:'状态'},{key:'act',label:'操作',fixed:true}];
const state={q:'',code:'',status:'全部',page:1,perPage:10,hidden:new Set(),sortBy:null,sortAsc:true};
function setActive(k){ document.querySelectorAll('.nav[data-k]').forEach(e=>e.classList.toggle('active',e.dataset.k===k)); }
function navTo(k){ setActive(k); const inv=document.querySelector('[data-view="inventory"]'), ph=document.querySelector('[data-view="placeholder"]'); if(k==='inventory'){inv.classList.add('active');ph.classList.remove('active');return;} inv.classList.remove('active');ph.classList.add('active'); const nav=NAVS.find(n=>n.k===k); document.getElementById('phTitle').textContent=nav.label; document.getElementById('phIcon').innerHTML=navSvg(nav.d); }
function visibleCols(){ return COLS.filter(c=>!state.hidden.has(c.key)); }
-function filtered(){ const rows=ITEMS.filter(it=>{ if(state.status!=='全部'&&it.status!==state.status)return false; if(state.q){const q=state.q.toLowerCase(); if(!it.name.toLowerCase().includes(q))return false;} if(state.code){const c=state.code.toLowerCase(); if(!it.code.toLowerCase().includes(c))return false;} return true; }); if(state.sortBy&&SORT_VAL[state.sortBy]){const v=SORT_VAL[state.sortBy];rows.sort((a,b)=>{const x=v(a),y=v(b);return (x
y?1:0)*(state.sortAsc?1:-1);});} return rows; }
+function filtered(){ const rows=ITEMS.filter(it=>{ if(state.status!=='全部'&&it.status!==state.status)return false; if(state.spec&&it.spec!==state.spec)return false; if(state.series&&it.series!==state.series)return false; if(state.wh&&it.supplier!==state.wh)return false; if(state.q){const q=state.q.toLowerCase(); if(!it.name.toLowerCase().includes(q))return false;} if(state.code){const c=state.code.toLowerCase(); if(!it.code.toLowerCase().includes(c))return false;} return true; }); if(state.sortBy&&SORT_VAL[state.sortBy]){const v=SORT_VAL[state.sortBy];rows.sort((a,b)=>{const x=v(a),y=v(b);return (xy?1:0)*(state.sortAsc?1:-1);});} return rows; }
function render(){
document.getElementById('statusVal').textContent=state.status==='全部'?'':state.status;
+ [['spec','chipSpec','specVal'],['series','chipSeries','seriesVal'],['wh','chipWh','whVal']].forEach(([k,cid,vid])=>{ document.getElementById(vid).textContent=state[k]||''; document.getElementById(cid).classList.toggle('on',!!state[k]); });
document.getElementById('chipStatus').classList.toggle('on',state.status!=='全部');
const cols=visibleCols();
document.getElementById('thead').innerHTML=''+cols.map(c=>{ const cls=(c.num?'num ':'')+(c.key==='act'?'act ':''); if(c.filter){const on=state.status!=='全部'; return `| ${c.label} | `;} if(c.sort){const on=state.sortBy===c.key; const ic=!on?'i-chevron-down':(state.sortAsc?'i-ic51':'i-chevron-down'); return `${c.label} | `;} return `${c.label} | `; }).join('')+'
';
@@ -141,9 +146,12 @@ function filterStatus(s){ state.status=s; state.page=1; navTo('inventory'); rend
// 列头排序:无→升→降→无 循环(真实实现为服务端全局排序)
function toggleSort(k){ if(state.sortBy!==k){state.sortBy=k;state.sortAsc=true;} else if(state.sortAsc){state.sortAsc=false;} else {state.sortBy=null;state.sortAsc=true;} state.page=1; render(); }
const SORT_VAL={qty:it=>it.qty,cost:it=>parseFloat(String(it.cost).replace(/[¥,]/g,''))||0,price:it=>parseFloat(String(it.price).replace(/[¥,]/g,''))||0,prodDate:it=>it.prodDate||'',inDate:it=>it.inDate||''};
-function clearFilters(){ state.q='';state.code='';state.status='全部';state.page=1; document.getElementById('searchInput').value=''; document.getElementById('codeInput').value=''; render(); }
+function clearFilters(){ state.q='';state.code='';state.status='全部';state.spec=null;state.series=null;state.wh=null;state.page=1; document.getElementById('searchInput').value=''; document.getElementById('codeInput').value=''; render(); }
function openFilter(kind,t){ openMenu(t,STATUS.map(v=>({v,label:v,sel:v===state.status})),v=>{ state.status=v; state.page=1; closeMenus(); render(); }); }
+// 维度筛选 chip(规格/系列/仓库,2026-07-07 从列头漏斗上移;真实实现为多选)
+const DIM_KEY={spec:'spec',series:'series',wh:'supplier'};
+function openDimFilter(dim,t){ const key=DIM_KEY[dim]; const opts=['全部',...new Set(ITEMS.map(it=>it[key]).filter(Boolean))]; openMenu(t,opts.map(v=>({v,label:v,sel:(state[dim]||'全部')===v})),v=>{ state[dim]=v==='全部'?null:v; state.page=1; closeMenus(); render(); }); }
document.getElementById('chipStatus').onclick=e=>openFilter('status',e.currentTarget);
const COL_KEY='jiu-inv-hidden';
function saveHidden(){ try{ localStorage.setItem(COL_KEY, JSON.stringify([...state.hidden])); }catch(e){} }
diff --git a/client/lib/screens/inventory/inventory_list_screen.dart b/client/lib/screens/inventory/inventory_list_screen.dart
index e71cc92..df8160e 100644
--- a/client/lib/screens/inventory/inventory_list_screen.dart
+++ b/client/lib/screens/inventory/inventory_list_screen.dart
@@ -206,7 +206,25 @@ class _InventoryListScreenState extends ConsumerState {
if (v != null && mounted) _applyStatusFilter(v);
}
- // ── 多选漏斗(规格/系列/仓库) ──
+ // ── 工具栏多选 chip(规格/系列/仓库,2026-07-07 从列头漏斗上移) ──
+ Widget _multiChip(String label, List options, Set selected,
+ ValueChanged> onChanged) {
+ return Builder(
+ builder: (chipCtx) => DsChip(
+ label: label,
+ value: selected.isEmpty
+ ? null
+ : selected.length == 1
+ ? selected.first
+ : '${selected.length} 项',
+ onTap: () => _openMultiFilter(chipCtx,
+ options: options, selected: selected, onChanged: onChanged),
+ onClear: () => onChanged({}),
+ ),
+ );
+ }
+
+ // ── 多选菜单(规格/系列/仓库,chip 锚定) ──
void _openMultiFilter(
BuildContext anchorContext, {
required List options,
@@ -908,11 +926,40 @@ class _InventoryListScreenState extends ConsumerState {
);
}
- // 桌面 toolbar:合一搜索框 + 重置
- // (状态筛选 chip 已按用户口径删除,状态列漏斗保留)
+ // 桌面 toolbar:搜索 + 筛选 chips + 重置
+ // (2026-07-07 用户口径反转:筛选全部上移工具栏,形式
+ // 参照出库管理;列头只留排序,漏斗退役)
return Row(
children: [
SizedBox(width: 260, child: searchField),
+ const SizedBox(width: AppDims.sp2),
+ _multiChip('规格', specOptions, _filterSpec, (v) {
+ setState(() => _filterSpec = v);
+ ref
+ .read(inventoryListProvider.notifier)
+ .setSpec(v.toList());
+ }),
+ const SizedBox(width: AppDims.sp2),
+ _multiChip('系列', seriesOptions, _filterSeries, (v) {
+ setState(() => _filterSeries = v);
+ ref
+ .read(inventoryListProvider.notifier)
+ .setSeries(v.toList());
+ }),
+ const SizedBox(width: AppDims.sp2),
+ _multiChip('仓库', warehouseOptions, _filterWarehouse,
+ (v) => setState(() => _filterWarehouse = v)),
+ const SizedBox(width: AppDims.sp2),
+ Builder(
+ builder: (chipCtx) => DsChip(
+ label: '状态',
+ value: _statusFilter == '全部'
+ ? null
+ : _statusFilter,
+ onTap: () => _openStatusMenu(chipCtx),
+ onClear: () => _applyStatusFilter('全部'),
+ ),
+ ),
const Spacer(),
DsButton('重置', small: true, onPressed: _clearFilters),
],
@@ -927,46 +974,8 @@ class _InventoryListScreenState extends ConsumerState {
},
emptyText: '没有匹配的商品 · 试试调整筛选或搜索',
columns: visibleCols.map((c) {
+ // 筛选已全部上移工具栏 chips(2026-07-07),列头只留排序。
final Widget? header = switch (c.key) {
- 'spec' => DsFilterHeader(
- label: c.label,
- filtered: _filterSpec.isNotEmpty,
- onOpen: (bctx) => _openMultiFilter(bctx,
- options: specOptions,
- selected: _filterSpec, onChanged: (v) {
- setState(() => _filterSpec = v);
- ref
- .read(inventoryListProvider.notifier)
- .setSpec(v.toList());
- }),
- ),
- 'series' => DsFilterHeader(
- label: c.label,
- filtered: _filterSeries.isNotEmpty,
- onOpen: (bctx) => _openMultiFilter(bctx,
- options: seriesOptions,
- selected: _filterSeries, onChanged: (v) {
- setState(() => _filterSeries = v);
- ref
- .read(inventoryListProvider.notifier)
- .setSeries(v.toList());
- }),
- ),
- 'warehouse' => DsFilterHeader(
- label: c.label,
- filtered: _filterWarehouse.isNotEmpty,
- onOpen: (bctx) => _openMultiFilter(bctx,
- options: warehouseOptions,
- selected: _filterWarehouse,
- onChanged: (v) =>
- setState(() => _filterWarehouse = v)),
- ),
- // 原型 COLS 里带 filter 的列只有状态(单选)。
- 'status' => DsFilterHeader(
- label: c.label,
- filtered: _statusFilter != '全部',
- onOpen: _openStatusMenu,
- ),
// 可排序列(原型 COLS sort:true,服务端全局排序)
'qty' ||
'cost' ||
diff --git a/client/test/golden/goldens/inventory_list_a.png b/client/test/golden/goldens/inventory_list_a.png
index 3b5a336..1c57b1c 100644
Binary files a/client/test/golden/goldens/inventory_list_a.png and b/client/test/golden/goldens/inventory_list_a.png differ
diff --git a/client/test/golden/goldens/inventory_list_b.png b/client/test/golden/goldens/inventory_list_b.png
index d9f1879..5ce830e 100644
Binary files a/client/test/golden/goldens/inventory_list_b.png and b/client/test/golden/goldens/inventory_list_b.png differ
diff --git a/client/test/golden/goldens/inventory_list_c.png b/client/test/golden/goldens/inventory_list_c.png
index e6f88fa..f621bd9 100644
Binary files a/client/test/golden/goldens/inventory_list_c.png and b/client/test/golden/goldens/inventory_list_c.png differ