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 (xy?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