feat(client): 库存筛选全部上移工具栏(参照出库管理),列头漏斗退役

- 工具栏:搜索 + 规格/系列/仓库 多选 chip(值/N 项 摘要 + × 单清)+ 状态
  单选 chip + 重置;列头只留 5 列排序箭头
- 原型 inventory.html 同步:toolbar 三 chip + openDimFilter、COLS 状态去
  filter、clearFilters/chip 值渲染跟进
- 桌面库存 golden 重录 ×3

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
wangjia
2026-07-07 17:01:54 +08:00
parent c7f36b2d72
commit e0d9b8ab09
5 changed files with 62 additions and 45 deletions
+11 -3
View File
@@ -59,6 +59,10 @@
<div class="toolbar">
<div class="searchbox"><svg viewBox="0 0 24 24"><use href="#i-search"/></svg><input id="searchInput" placeholder="商品名 / 拼音" oninput="state.q=this.value; state.page=1; render()"></div>
<div class="searchbox code"><svg viewBox="0 0 24 24"><use href="#i-search"/></svg><input id="codeInput" placeholder="商品编码" oninput="state.code=this.value; state.page=1; render()"></div>
<!-- 2026-07-07 用户口径:筛选全部上移工具栏(参照出库管理),列头漏斗退役、只留排序 -->
<div class="chip" id="chipSpec" onclick="openDimFilter('spec',this)">规格 <span class="cv" id="specVal"></span><svg viewBox="0 0 24 24"><use href="#i-chevron-down"/></svg></div>
<div class="chip" id="chipSeries" onclick="openDimFilter('series',this)">系列 <span class="cv" id="seriesVal"></span><svg viewBox="0 0 24 24"><use href="#i-chevron-down"/></svg></div>
<div class="chip" id="chipWh" onclick="openDimFilter('wh',this)">仓库 <span class="cv" id="whVal"></span><svg viewBox="0 0 24 24"><use href="#i-chevron-down"/></svg></div>
<div class="chip" id="chipStatus">状态 <span class="cv" id="statusVal"></span><svg viewBox="0 0 24 24"><use href="#i-chevron-down"/></svg></div>
<div class="sp"></div>
<div class="btn ghost sm" onclick="clearFilters()">重置</div>
@@ -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: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 (x<y?-1:x>y?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='<tr>'+cols.map(c=>{ const cls=(c.num?'num ':'')+(c.key==='act'?'act ':''); if(c.filter){const on=state.status!=='全部'; return `<th class="${cls}"><span class="fh ${on?'filtered':''}" onclick="openFilter('${c.filter}',this)">${c.label} <svg class="funnel" viewBox="0 0 24 24"><use href="#i-ic22"/></svg></span></th>`;} if(c.sort){const on=state.sortBy===c.key; const ic=!on?'i-chevron-down':(state.sortAsc?'i-ic51':'i-chevron-down'); return `<th class="${cls}"><span class="sh ${on?'on':''}" onclick="toggleSort('${c.key}')">${c.label} <svg class="sort-ic" viewBox="0 0 24 24"><use href="#${ic}"/></svg></span></th>`;} return `<th class="${cls}">${c.label}</th>`; }).join('')+'</tr>';
@@ -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){} }
@@ -206,7 +206,25 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
if (v != null && mounted) _applyStatusFilter(v);
}
// ── 多选漏斗(规格/系列/仓库 ──
// ── 工具栏多选 chip(规格/系列/仓库,2026-07-07 从列头漏斗上移 ──
Widget _multiChip(String label, List<String> options, Set<String> selected,
ValueChanged<Set<String>> 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<String> options,
@@ -908,11 +926,40 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
);
}
// 桌面 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<InventoryListScreen> {
},
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' ||
Binary file not shown.

Before

Width:  |  Height:  |  Size: 414 KiB

After

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 409 KiB

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 KiB

After

Width:  |  Height:  |  Size: 422 KiB