docs(design): 移动出入库列表原型补齐桌面近期功能(简化版)

- 新状态词对齐桌面/后端:已通过/已驳回 → 已审核/已拒绝,新增 部分退单/已退单
  徽章与筛选 pill(return_state: partial/full)
- 新增「详细搜索」pill 入口 + 底部 sheet(与 Flutter 详细搜索同字段集,竖排
  单列,select 内联展开 m-opt 选项,激活计数高亮)
- 详情 sheet 补 审核员 行与退单徽章;已审核单加「退单」入口
- 新增 退单 sheet(勾选明细+数量+全部退单,入库=从库存删除/出库=加回库存文案)
- 确认进价(入库)升级为 sheet 表单;出库新增 确认售价 sheet(先卖后定价)
- 出库详情补 合计利润 行(管理员可见)与明细待定价徽章

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-04 09:24:32 +08:00
parent 9b37568fdb
commit 12952ea9ae
2 changed files with 204 additions and 37 deletions
@@ -9,6 +9,11 @@
<link rel="stylesheet" href="../mobile-atoms.css">
<style>
body{margin:0; font-family:var(--font); color:var(--text); background:var(--page); -webkit-font-smoothing:antialiased;}
/* ---- 状态徽章(新词,与桌面 stock-in-list 一致,待提升至 atoms.css ---- */
.badge.b-已审核{background:var(--success-bg); color:var(--success);}
.badge.b-已拒绝{background:var(--danger-bg); color:var(--danger);}
.badge.b-部分退单{background:var(--warn-bg); color:var(--warn);}
.badge.b-已退单{background:var(--danger-bg); color:var(--danger);}
</style>
</head>
<body>
@@ -29,20 +34,40 @@
<script src="mobile-shell.js"></script>
<script>
const ORDERS=[
{no:'RK-20260620-009',party:'鼎晟调货',items:2,amount:'¥5,360',status:'已通过',date:'2026-06-20',by:'王经理',goods:'茅台 飞天 53°、五粮液 普五 52°',pending:1,lines:[{n:'茅台 飞天 53°',s:'500ml×6',q:1,a:'¥2,680'},{n:'五粮液 普五 52°',s:'500ml×6',q:1,a:'¥2,680'}]},
{no:'RK-20260620-006',party:'鼎晟供应链',items:3,amount:'¥12,800',status:'待审核',date:'2026-06-20',by:'王经理',goods:'茅台 王子酒、习酒 窖藏 1988',lines:[{n:'茅台 王子酒',s:'500ml×6',q:4,a:'¥4,800'},{n:'习酒 窖藏 1988',s:'500ml×6',q:2,a:'¥4,000'},{n:'茅台 迎宾酒',s:'500ml×6',q:4,a:'¥4,000'}]},
{no:'RK-20260620-005',party:'贵州茅台经销',items:2,amount:'¥80,400',status:'已通过',date:'2026-06-20',by:'王经理',goods:'茅台 飞天 53°、茅台 1935',lines:[{n:'茅台 飞天 53°',s:'500ml×6',q:20,a:'¥53,600'},{n:'茅台 1935',s:'500ml×6',q:10,a:'¥26,800'}]},
{no:'RK-20260619-004',party:'五粮液华东仓',items:2,amount:'¥33,600',status:'已通过',date:'2026-06-19',by:'李采购',goods:'五粮液 普五 52°、五粮液 1618',lines:[{n:'五粮液 普五 52°',s:'500ml×6',q:8,a:'¥16,800'},{n:'五粮液 1618',s:'500ml×6',q:8,a:'¥16,800'}]},
{no:'RK-20260619-003',party:'剑南春直营',items:1,amount:'¥4,380',status:'已驳回',date:'2026-06-19',by:'李采购',goods:'剑南春 水晶剑',lines:[{n:'剑南春 水晶剑',s:'500ml×6',q:10,a:'¥4,380'}]},
{no:'RK-20260618-002',party:'泸州老窖仓',items:2,amount:'¥16,000',status:'已通过',date:'2026-06-18',by:'王经理',goods:'泸州老窖 特曲、国窖 1573',lines:[{n:'泸州老窖 特曲',s:'500ml×6',q:25,a:'¥8,000'},{n:'国窖 1573',s:'500ml×6',q:8,a:'¥8,000'}]},
{no:'RK-20260618-001',party:'洋河股份',items:2,amount:'¥14,800',status:'草稿',date:'2026-06-18',by:'王经理',goods:'洋河 梦之蓝 M6、洋河 海之蓝',lines:[{n:'洋河 梦之蓝 M6',s:'500ml×6',q:12,a:'¥7,440'},{n:'洋河 海之蓝',s:'500ml×6',q:16,a:'¥7,360'}]},
{no:'RK-20260620-009',party:'鼎晟调货',items:2,amount:'¥5,360',status:'已审核',ret:'部分退单',date:'2026-06-20',by:'王经理',auditor:'张主管',goods:'茅台 飞天 53°、五粮液 普五 52°',pending:1,lines:[{n:'茅台 飞天 53°',s:'500ml×6',q:1,a:'待定价',pend:true},{n:'五粮液 普五 52°',s:'500ml×6',q:1,a:'¥2,680'}]},
{no:'RK-20260620-006',party:'鼎晟供应链',items:3,amount:'¥12,800',status:'待审核',date:'2026-06-20',by:'王经理',auditor:'',goods:'茅台 王子酒、习酒 窖藏 1988',lines:[{n:'茅台 王子酒',s:'500ml×6',q:4,a:'¥4,800'},{n:'习酒 窖藏 1988',s:'500ml×6',q:2,a:'¥4,000'},{n:'茅台 迎宾酒',s:'500ml×6',q:4,a:'¥4,000'}]},
{no:'RK-20260620-005',party:'贵州茅台经销',items:2,amount:'¥80,400',status:'已审核',date:'2026-06-20',by:'王经理',auditor:'张主管',goods:'茅台 飞天 53°、茅台 1935',lines:[{n:'茅台 飞天 53°',s:'500ml×6',q:20,a:'¥53,600'},{n:'茅台 1935',s:'500ml×6',q:10,a:'¥26,800'}]},
{no:'RK-20260619-004',party:'五粮液华东仓',items:2,amount:'¥33,600',status:'已审核',ret:'已退单',date:'2026-06-19',by:'李采购',auditor:'张主管',goods:'五粮液 普五 52°、五粮液 1618',lines:[{n:'五粮液 普五 52°',s:'500ml×6',q:8,a:'¥16,800'},{n:'五粮液 1618',s:'500ml×6',q:8,a:'¥16,800'}]},
{no:'RK-20260619-003',party:'剑南春直营',items:1,amount:'¥4,380',status:'已拒绝',date:'2026-06-19',by:'李采购',auditor:'张主管',goods:'剑南春 水晶剑',lines:[{n:'剑南春 水晶剑',s:'500ml×6',q:10,a:'¥4,380'}]},
{no:'RK-20260618-002',party:'泸州老窖仓',items:2,amount:'¥16,000',status:'已审核',date:'2026-06-18',by:'王经理',auditor:'王经理',goods:'泸州老窖 特曲、国窖 1573',lines:[{n:'泸州老窖 特曲',s:'500ml×6',q:25,a:'¥8,000'},{n:'国窖 1573',s:'500ml×6',q:8,a:'¥8,000'}]},
{no:'RK-20260618-001',party:'洋河股份',items:2,amount:'¥14,800',status:'草稿',date:'2026-06-18',by:'王经理',auditor:'',goods:'洋河 梦之蓝 M6、洋河 海之蓝',lines:[{n:'洋河 梦之蓝 M6',s:'500ml×6',q:12,a:'¥7,440'},{n:'洋河 海之蓝',s:'500ml×6',q:16,a:'¥7,360'}]},
];
const ST=['全部','草稿','待审核','已通过','已驳回'];
/* 状态词与桌面/后端对齐:主状态 4 词 + 退单态 2 词(return_state: partial/full */
const ST=['全部','草稿','待审核','已审核','已拒绝','部分退单','已退单'];
const OPS=['全部','王经理','李采购'];
const REVIEWERS=['全部','张主管','王经理'];
const DATES=['全部时间','近 7 天','近 30 天','本月'];
const state={q:'',status:'全部'};
/* ---- 详细搜索条件(与 Flutter _AdvSearchDialog 同一字段集,移动为竖排简化版) ---- */
const ADV_DEF={no:'',product:'',batch:'',party:'全部',series:'全部',spec:'全部',op:'全部',reviewer:'全部',status:'全部',date:'全部时间'};
let adv={...ADV_DEF};
function advCount(){ return Object.keys(ADV_DEF).filter(k=>adv[k]!==ADV_DEF[k]).length; }
function filtered(){ return ORDERS.filter(o=>{ if(state.status!=='全部'&&o.status!==state.status)return false; if(state.q){const q=state.q.toLowerCase(); if(!o.no.toLowerCase().includes(q)&&!o.party.toLowerCase().includes(q)&&!(o.goods||'').toLowerCase().includes(q))return false;} return true; }); }
function filtered(){ return ORDERS.filter(o=>{
const st=state.status;
if(st!=='全部'&&o.status!==st&&o.ret!==st)return false;
if(state.q){const q=state.q.toLowerCase(); if(!o.no.toLowerCase().includes(q)&&!o.party.toLowerCase().includes(q)&&!(o.goods||'').toLowerCase().includes(q))return false;}
if(adv.no&&!o.no.toLowerCase().includes(adv.no.toLowerCase()))return false;
if(adv.product){const p=adv.product.toLowerCase(); if(!(o.lines||[]).some(l=>l.n.toLowerCase().includes(p)))return false;}
if(adv.party!=='全部'&&o.party!==adv.party)return false;
if(adv.op!=='全部'&&o.by!==adv.op)return false;
if(adv.reviewer!=='全部'&&o.auditor!==adv.reviewer)return false;
if(adv.status!=='全部'&&o.status!==adv.status&&o.ret!==adv.status)return false;
return true; }); }
function render(){
document.getElementById('pills').innerHTML=ST.map(s=>`<div class="m-pill ${s===state.status?'on':''}" onclick="setSt('${s}')">${s}</div>`).join('');
const n=advCount();
document.getElementById('pills').innerHTML=ST.map(s=>`<div class="m-pill ${s===state.status?'on':''}" onclick="setSt('${s}')">${s}</div>`).join('')
+`<div class="m-pill ${n?'on':''}" onclick="openAdv()"><svg width="13" height="13" viewBox="0 0 24 24" style="vertical-align:-2px"><use href="#i-ic21"/></svg> 详细搜索${n?' · '+n:''}</div>`;
const rows=filtered();
document.getElementById('cnt').textContent=`入库单 · 共 ${rows.length}`;
const L=document.getElementById('list');
@@ -50,29 +75,84 @@ function render(){
L.innerHTML=rows.map(o=>`<a class="m-card" onclick="openOrder('${o.no}')">
<div class="m-row">
<div class="mc-main"><div class="mc-nm">${o.no}</div><div class="mc-sub">${o.party} · ${o.items} 项</div></div>
<div class="mc-right"><span class="badge b-${o.status}">${o.status}</span>${o.pending?'<span class="badge b-待定价">待定价</span>':''}<span class="mc-amt">${o.amount}</span></div>
<div class="mc-right"><span class="badge b-${o.status}">${o.status}</span>${o.ret?`<span class="badge b-${o.ret}">${o.ret}</span>`:''}${o.pending?'<span class="badge b-待定价">待定价</span>':''}<span class="mc-amt">${o.amount}</span></div>
<div class="mc-chev"><svg viewBox="0 0 24 24"><use href="#i-chevron-right"/></svg></div>
</div>
<div class="mc-foot"><svg width="13" height="13" viewBox="0 0 24 24" stroke-width="1.9" style="color:var(--muted)"><use href="#i-ic24"/></svg>${o.date} · 经手人 ${o.by}</div>
<div class="mc-foot"><svg width="13" height="13" viewBox="0 0 24 24" stroke-width="1.9" style="color:var(--muted)"><use href="#i-ic24"/></svg>${o.date} · 经手人 ${o.by}${o.auditor?' · 审核 '+o.auditor:''}</div>
</a>`).join('');
}
function setSt(s){ state.status=s; render(); }
function actions(st){
/* ==================== 详细搜索(底部 sheet,竖排单列) ==================== */
let advOpen=''; // 当前展开的下拉字段
function advInput(key,label,ph){ return `<div class="m-field"><label>${label}</label><input class="m-input" value="${adv[key]}" placeholder="${ph}" oninput="adv.${key}=this.value"></div>`; }
function advSelect(key,label,opts){
const open=advOpen===key;
const optRows=open?`<div style="border:1px solid var(--border);border-top:none;border-radius:0 0 var(--r-md) var(--r-md);padding:0 12px;background:var(--surface)">${opts.map(v=>`<div class="m-opt ${v===adv[key]?'sel':''}" onclick="advPick('${key}','${v}')">${v}<svg class="ck" viewBox="0 0 24 24"><use href="#i-check"/></svg></div>`).join('')}</div>`:'';
return `<div class="m-field"><label>${label}</label><div class="m-select" ${open?'style="border-radius:var(--r-md) var(--r-md) 0 0"':''} onclick="advToggle('${key}')"><span class="${adv[key]==='全部'||adv[key]==='全部时间'?'ph':''}">${adv[key]}</span><svg viewBox="0 0 24 24"><use href="#i-chevron-down"/></svg></div>${optRows}</div>`;
}
function advToggle(k){ advOpen=advOpen===k?'':k; drawAdv(); }
function advPick(k,v){ adv[k]=v; advOpen=''; drawAdv(); }
function drawAdv(){
openSheet('详细搜索',
advInput('no','单据编号','输入单据编号')
+advSelect('party','供应商',['全部','鼎晟调货','鼎晟供应链','贵州茅台经销','五粮液华东仓','剑南春直营','泸州老窖仓','洋河股份'])
+advInput('product','商品信息','酒名 / 商品编码')
+advSelect('series','系列',['全部','飞天','1935','普五','水晶剑','窖藏'])
+advSelect('spec','规格',['全部','53° 500ml×6','52° 500ml×6','45° 500ml×6'])
+advInput('batch','批次 · 编号 · 物流码','批次 / 商品编码 / 物流码')
+advSelect('status','状态',ST)
+advSelect('op','入库员',OPS)
+advSelect('reviewer','审核人',REVIEWERS)
+advSelect('date','单据日期',DATES)
+`<div style="display:flex;gap:10px;margin-top:16px"><div class="btn ghost" style="flex:1;justify-content:center" onclick="resetAdv()">重置</div><div class="btn primary" style="flex:2;justify-content:center" onclick="applyAdv()">确认</div></div>`);
}
function openAdv(){ advOpen=''; drawAdv(); }
function resetAdv(){ adv={...ADV_DEF}; advOpen=''; drawAdv(); }
function applyAdv(){ closeSheet(); render(); }
/* ==================== 单据详情 / 审核 / 确认进价 / 退单 ==================== */
function actions(o){
const st=o.status;
if(st==='草稿') return '<div class="btn primary" style="flex:1;justify-content:center" onclick="mToast(\'已提交审核 ✓\');closeSheet()">提交审核</div>';
if(st==='待审核') return '<div class="btn danger" style="flex:1;justify-content:center" onclick="mToast(\'已驳回\');closeSheet()">驳回</div><div class="btn primary" style="flex:1;justify-content:center" onclick="mToast(\'已审核通过 ✓\');closeSheet()">通过</div>';
if(st==='已驳回') return '<div class="btn primary" style="flex:1;justify-content:center" onclick="mToast(\'已重新提交 ✓\');closeSheet()">重新提交</div>';
if(st==='待审核') return '<div class="btn danger" style="flex:1;justify-content:center" onclick="mToast(\'已拒绝\');closeSheet()">拒绝</div><div class="btn primary" style="flex:1;justify-content:center" onclick="mToast(\'已审核通过 ✓\');closeSheet()">通过</div>';
if(st==='已拒绝') return '<div class="btn primary" style="flex:1;justify-content:center" onclick="mToast(\'已重新提交 ✓\');closeSheet()">重新提交</div>';
if(st==='已审核'&&o.ret!=='已退单') return `<div class="btn danger" style="flex:1;justify-content:center" onclick="openReturn('${o.no}')">退单</div><div class="btn ghost" style="flex:1;justify-content:center" onclick="closeSheet()">关闭</div>`;
return '<div class="btn ghost" style="flex:1;justify-content:center" onclick="closeSheet()">关闭</div>';
}
function openOrder(no){ const o=ORDERS.find(x=>x.no===no);
const pendRow=o.pending?`<div class="drow"><span>成本</span><b><span class="badge b-待定价">含待定价</span></b></div>`:'';
const confirmBtn=(o.pending&&o.status==='已通过')?`<div class="btn primary" style="flex:1;justify-content:center" onclick="mToast('进价已确认,已补应付差额 ✓');closeSheet()"><svg viewBox="0 0 24 24"><use href="#i-edit"/></svg>确认进价</div>`:'';
const confirmBtn=(o.pending&&o.status==='已审核')?`<div class="btn primary" style="flex:1;justify-content:center" onclick="openConfirm('${o.no}')"><svg viewBox="0 0 24 24"><use href="#i-edit"/></svg>确认进价</div>`:'';
const lns=o.lines||[];
const lineRows=lns.map((l,i)=>`<div class="drow"${i===lns.length-1?' style="border-bottom:none"':''}><span style="color:var(--text);line-height:1.45">${l.n}<br><span style="color:var(--faint);font-size:var(--fs-xs)">${l.s} · ×${l.q}</span></span><b style="font-family:var(--font-mono)">${l.a}</b></div>`).join('');
openSheet('入库单详情', `<div class="drow"><span>单号</span><b style="font-family:var(--font-mono)">${o.no}</b></div><div class="drow"><span>供应商</span><b>${o.party}</b></div><div class="drow"><span>单据日期</span><b>${o.date}</b></div><div class="drow"><span>经手人</span><b>${o.by}</b></div><div class="drow"><span>合计金额</span><b style="font-family:var(--font-mono)">${o.amount}</b></div>${pendRow}<div class="drow" style="border-bottom:none"><span>状态</span><b><span class="badge b-${o.status}">${o.status}</span></b></div>
const lineRows=lns.map((l,i)=>`<div class="drow"${i===lns.length-1?' style="border-bottom:none"':''}><span style="color:var(--text);line-height:1.45">${l.n}<br><span style="color:var(--faint);font-size:var(--fs-xs)">${l.s} · ×${l.q}</span></span><b style="font-family:var(--font-mono)">${l.pend?'<span class="badge b-待定价">待定价</span>':l.a}</b></div>`).join('');
openSheet('入库单详情', `<div class="drow"><span>单号</span><b style="font-family:var(--font-mono)">${o.no}</b></div><div class="drow"><span>供应商</span><b>${o.party}</b></div><div class="drow"><span>单据日期</span><b>${o.date}</b></div><div class="drow"><span>入库员</span><b>${o.by}</b></div><div class="drow"><span>审核员</span><b>${o.auditor||'—'}</b></div><div class="drow"><span>合计金额</span><b style="font-family:var(--font-mono)">${o.amount}</b></div>${pendRow}<div class="drow" style="border-bottom:none"><span>状态</span><b><span class="badge b-${o.status}">${o.status}</span>${o.ret?` <span class="badge b-${o.ret}">${o.ret}</span>`:''}</b></div>
<div class="pe-card" style="margin-top:14px"><div class="pe-h"><span class="pe-ic"><svg viewBox="0 0 24 24"><use href="#i-ic33"/></svg></span>商品明细 · ${lns.length} 项</div><div class="pe-b" style="padding-top:2px;padding-bottom:2px">${lineRows}</div></div>
${o.pending?'<div style="margin-top:12px;font-size:var(--fs-sm);line-height:1.6;color:var(--warn)">调货「先卖后定价」:进价待定,价格确定后点「确认进价」前向补齐成本与应付。</div>':''}
<div style="display:flex;gap:10px;margin-top:16px">${confirmBtn}${actions(o.status)}</div>`);
<div style="display:flex;gap:10px;margin-top:16px">${confirmBtn}${actions(o)}</div>`);
}
/* ---- 确认进价(对齐桌面 ovConfirm,简化为 sheet ---- */
function openConfirm(no){ const o=ORDERS.find(x=>x.no===no); const pend=(o.lines||[]).filter(l=>l.pend);
openSheet('确认进价', `<div class="notice info" style="margin-bottom:14px"><svg viewBox="0 0 24 24"><use href="#i-ic42"/></svg><div>调货「先卖后定价」:填写真实进价后,系统前向补齐库存成本、已出库成本快照,并按差额补一条应付流水——不反审核、不影响售价 / 应收。</div></div>`
+pend.map(l=>`<div class="m-field"><label>${l.n} · 数量 ${l.q}</label><input class="m-input" type="number" inputmode="decimal" placeholder="真实进价 0.00"></div>`).join('')
+`<div style="display:flex;gap:10px;margin-top:16px"><div class="btn ghost" style="flex:1;justify-content:center" onclick="closeSheet()">取消</div><div class="btn primary" style="flex:2;justify-content:center" onclick="mToast('进价已确认,已前向补齐成本并补应付差额 ✓');closeSheet()">确认并补差额</div></div>`);
}
/* ---- 退单(对齐桌面 ovReturn:勾选明细 + 数量,入库=从库存删除 −) ---- */
let retNo='', retStage={};
function openReturn(no){ retNo=no; retStage={}; drawReturn(); }
function toggleRet(i,max){ if(retStage[i]!=null) delete retStage[i]; else retStage[i]=max; drawReturn(); }
function setRetQty(i,v,max){ retStage[i]=Math.min(Math.max(1,Math.floor(+v||0)),max); drawReturnSummary(); }
function stageAllRet(){ const o=ORDERS.find(x=>x.no===retNo); (o.lines||[]).forEach((l,i)=>retStage[i]=l.q); drawReturn(); }
function drawReturnSummary(){ const el=document.getElementById('retSum'); if(!el)return; const keys=Object.keys(retStage); const qty=keys.reduce((s,k)=>s+(+retStage[k]||0),0); el.textContent=keys.length?`已退单 ${keys.length} 项 · 从库存移除 ${qty}`:'未选择退单明细'; }
function drawReturn(){ const o=ORDERS.find(x=>x.no===retNo);
openSheet('退单', `<div class="notice info" style="margin-bottom:14px"><svg viewBox="0 0 24 24"><use href="#i-ic42"/></svg><div>提交后,退单明细对应的库存将「从库存删除」。退单明细不会删除,以红色「已退单」标注,且不可撤销。</div></div>`
+(o.lines||[]).map((l,i)=>{ const st=retStage[i]!=null;
return `<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px"><label style="display:flex;align-items:center;gap:9px;flex:1;cursor:pointer"><input type="checkbox" ${st?'checked':''} onchange="toggleRet(${i},${l.q})"><span><div style="font-weight:600;color:var(--heading);font-size:var(--fs-body)">${l.n}</div><div style="color:var(--faint);font-size:var(--fs-xs)">${l.s} · 数量 ${l.q}</div></span></label><input class="m-input" style="width:104px" type="number" min="1" max="${l.q}" value="${st?retStage[i]:''}" ${st?'':'disabled'} placeholder="退货数量" oninput="setRetQty(${i},this.value,${l.q})"></div>`; }).join('')
+`<div style="display:flex;align-items:center;gap:10px;margin-top:8px"><div class="btn ghost sm" onclick="stageAllRet()">全部退单</div><span id="retSum" style="font-size:var(--fs-sm);color:var(--muted)">未选择退单明细</span></div>
<div style="display:flex;gap:10px;margin-top:16px"><div class="btn ghost" style="flex:1;justify-content:center" onclick="closeSheet()">取消</div><div class="btn danger" style="flex:2;justify-content:center" onclick="submitReturn()">提交退单</div></div>`);
drawReturnSummary();
}
function submitReturn(){ const keys=Object.keys(retStage); if(!keys.length){mToast('请先选择要退的明细');return;} const o=ORDERS.find(x=>x.no===retNo); o.ret=keys.length<(o.lines||[]).length?'部分退单':'已退单'; closeSheet(); render(); mToast('退单成功 ✓'); }
render();
</script>
</body>
@@ -7,7 +7,14 @@
<link rel="stylesheet" href="../tokens.css">
<link rel="stylesheet" href="../atoms.css">
<link rel="stylesheet" href="../mobile-atoms.css">
<style>body{margin:0; font-family:var(--font); color:var(--text); background:var(--page); -webkit-font-smoothing:antialiased;}</style>
<style>
body{margin:0; font-family:var(--font); color:var(--text); background:var(--page); -webkit-font-smoothing:antialiased;}
/* ---- 状态徽章(新词,与桌面 stock-out-list 一致,待提升至 atoms.css ---- */
.badge.b-已审核{background:var(--success-bg); color:var(--success);}
.badge.b-已拒绝{background:var(--danger-bg); color:var(--danger);}
.badge.b-部分退单{background:var(--warn-bg); color:var(--warn);}
.badge.b-已退单{background:var(--danger-bg); color:var(--danger);}
</style>
</head>
<body>
<div class="m-top" data-m-top data-title="出库管理"></div>
@@ -27,17 +34,39 @@
<script src="mobile-shell.js"></script>
<script>
const ORDERS=[
{no:'CK-20260620-008',party:'城东烟酒行',items:2,amount:'¥18,600',status:'待审核',date:'2026-06-20',by:'王经理',goods:'茅台 飞天 53°、五粮液 普五 52°',lines:[{n:'茅台 飞天 53°',s:'500ml×6',q:4,a:'¥12,600'},{n:'五粮液 普五 52°',s:'500ml×6',q:3,a:'¥6,000'}]},
{no:'CK-20260620-007',party:'宴汇大酒店',items:2,amount:'¥6,400',status:'已通过',date:'2026-06-20',by:'李销售',goods:'剑南春 水晶剑、洋河 海之蓝',lines:[{n:'剑南春 水晶剑',s:'500ml×6',q:4,a:'¥3,200'},{n:'洋河 海之蓝',s:'500ml×6',q:8,a:'¥3,200'}]},
{no:'CK-20260619-006',party:'金樽会所',items:2,amount:'¥41,000',status:'已通过',date:'2026-06-19',by:'王经理',goods:'茅台 飞天 53°、国窖 1573',lines:[{n:'茅台 飞天 53°',s:'500ml×6',q:10,a:'¥28,000'},{n:'国窖 1573',s:'500ml×6',q:13,a:'¥13,000'}]},
{no:'CK-20260619-005',party:'喜宴婚庆',items:2,amount:'¥9,900',status:'已驳回',date:'2026-06-19',by:'李销售',goods:'泸州老窖 特曲、汾酒 青花 20',lines:[{n:'泸州老窖 特曲',s:'500ml×6',q:10,a:'¥5,000'},{n:'汾酒 青花 20',s:'500ml×6',q:7,a:'¥4,900'}]},
{no:'CK-20260618-004',party:'城东烟酒行',items:2,amount:'¥12,500',status:'已通过',date:'2026-06-18',by:'王经理',goods:'西凤酒 旗舰版、郎酒 红花郎 15',lines:[{n:'西凤酒 旗舰版',s:'500ml×6',q:13,a:'¥6,500'},{n:'郎酒 红花郎 15',s:'500ml×6',q:10,a:'¥6,000'}]},
{no:'CK-20260620-008',party:'城东烟酒行',items:2,amount:'¥18,600',profit:'¥3,400',status:'待审核',date:'2026-06-20',by:'王经理',auditor:'',goods:'茅台 飞天 53°、五粮液 普五 52°',lines:[{n:'茅台 飞天 53°',s:'500ml×6',q:4,a:'¥12,600'},{n:'五粮液 普五 52°',s:'500ml×6',q:3,a:'¥6,000'}]},
{no:'CK-20260620-007',party:'宴汇大酒店',items:2,amount:'¥6,400',profit:'¥980',status:'已审核',ret:'部分退单',date:'2026-06-20',by:'李销售',auditor:'张主管',goods:'剑南春 水晶剑、洋河 海之蓝',lines:[{n:'剑南春 水晶剑',s:'500ml×6',q:4,a:'¥3,200'},{n:'洋河 海之蓝',s:'500ml×6',q:8,a:'¥3,200'}]},
{no:'CK-20260619-006',party:'金樽会所',items:2,amount:'¥41,000',profit:'¥6,150',status:'已审核',date:'2026-06-19',by:'王经理',auditor:'张主管',goods:'茅台 飞天 53°、国窖 1573',pending:1,lines:[{n:'茅台 飞天 53°',s:'500ml×6',q:10,a:'¥28,000'},{n:'国窖 1573',s:'500ml×6',q:13,a:'待定价',pend:true}]},
{no:'CK-20260619-005',party:'喜宴婚庆',items:2,amount:'¥9,900',profit:'¥1,480',status:'已拒绝',date:'2026-06-19',by:'李销售',auditor:'张主管',goods:'泸州老窖 特曲、汾酒 青花 20',lines:[{n:'泸州老窖 特曲',s:'500ml×6',q:10,a:'¥5,000'},{n:'汾酒 青花 20',s:'500ml×6',q:7,a:'¥4,900'}]},
{no:'CK-20260618-004',party:'城东烟酒行',items:2,amount:'¥12,500',profit:'¥1,870',status:'已审核',ret:'已退单',date:'2026-06-18',by:'王经理',auditor:'王经理',goods:'西凤酒 旗舰版、郎酒 红花郎 15',lines:[{n:'西凤酒 旗舰版',s:'500ml×6',q:13,a:'¥6,500'},{n:'郎酒 红花郎 15',s:'500ml×6',q:10,a:'¥6,000'}]},
{no:'CK-20260618-003',party:'金樽会所',items:1,amount:'¥8,400',profit:'¥1,260',status:'草稿',date:'2026-06-18',by:'李销售',auditor:'',goods:'汾酒 青花 20',lines:[{n:'汾酒 青花 20',s:'500ml×6',q:12,a:'¥8,400'}]},
];
const ST=['全部','待审核','已通过','已驳回'];
/* 状态词与桌面/后端对齐:主状态 4 词 + 退单态 2 词(return_state: partial/full */
const ST=['全部','草稿','待审核','已审核','已拒绝','部分退单','已退单'];
const OPS=['全部','王经理','李销售'];
const REVIEWERS=['全部','张主管','王经理'];
const DATES=['全部时间','近 7 天','近 30 天','本月'];
const state={q:'',status:'全部'};
function filtered(){ return ORDERS.filter(o=>{ if(state.status!=='全部'&&o.status!==state.status)return false; if(state.q){const q=state.q.toLowerCase(); if(!o.no.toLowerCase().includes(q)&&!o.party.toLowerCase().includes(q)&&!(o.goods||'').toLowerCase().includes(q))return false;} return true; }); }
/* ---- 详细搜索条件(与 Flutter _AdvancedSearchDialog 同一字段集,移动为竖排简化版) ---- */
const ADV_DEF={no:'',product:'',batch:'',party:'全部',series:'全部',spec:'全部',op:'全部',reviewer:'全部',status:'全部',date:'全部时间'};
let adv={...ADV_DEF};
function advCount(){ return Object.keys(ADV_DEF).filter(k=>adv[k]!==ADV_DEF[k]).length; }
function filtered(){ return ORDERS.filter(o=>{
const st=state.status;
if(st!=='全部'&&o.status!==st&&o.ret!==st)return false;
if(state.q){const q=state.q.toLowerCase(); if(!o.no.toLowerCase().includes(q)&&!o.party.toLowerCase().includes(q)&&!(o.goods||'').toLowerCase().includes(q))return false;}
if(adv.no&&!o.no.toLowerCase().includes(adv.no.toLowerCase()))return false;
if(adv.product){const p=adv.product.toLowerCase(); if(!(o.lines||[]).some(l=>l.n.toLowerCase().includes(p)))return false;}
if(adv.party!=='全部'&&o.party!==adv.party)return false;
if(adv.op!=='全部'&&o.by!==adv.op)return false;
if(adv.reviewer!=='全部'&&o.auditor!==adv.reviewer)return false;
if(adv.status!=='全部'&&o.status!==adv.status&&o.ret!==adv.status)return false;
return true; }); }
function render(){
document.getElementById('pills').innerHTML=ST.map(s=>`<div class="m-pill ${s===state.status?'on':''}" onclick="setSt('${s}')">${s}</div>`).join('');
const n=advCount();
document.getElementById('pills').innerHTML=ST.map(s=>`<div class="m-pill ${s===state.status?'on':''}" onclick="setSt('${s}')">${s}</div>`).join('')
+`<div class="m-pill ${n?'on':''}" onclick="openAdv()"><svg width="13" height="13" viewBox="0 0 24 24" style="vertical-align:-2px"><use href="#i-ic21"/></svg> 详细搜索${n?' · '+n:''}</div>`;
const rows=filtered();
document.getElementById('cnt').textContent=`出库单 · 共 ${rows.length}`;
const L=document.getElementById('list');
@@ -45,26 +74,84 @@ function render(){
L.innerHTML=rows.map(o=>`<a class="m-card" onclick="openOrder('${o.no}')">
<div class="m-row">
<div class="mc-main"><div class="mc-nm">${o.no}</div><div class="mc-sub">${o.party} · ${o.items} 项</div></div>
<div class="mc-right"><span class="badge b-${o.status}">${o.status}</span><span class="mc-amt">${o.amount}</span></div>
<div class="mc-right"><span class="badge b-${o.status}">${o.status}</span>${o.ret?`<span class="badge b-${o.ret}">${o.ret}</span>`:''}${o.pending?'<span class="badge b-待定价">待定价</span>':''}<span class="mc-amt">${o.amount}</span></div>
<div class="mc-chev"><svg viewBox="0 0 24 24"><use href="#i-chevron-right"/></svg></div>
</div>
<div class="mc-foot"><svg width="13" height="13" viewBox="0 0 24 24" stroke-width="1.9" style="color:var(--muted)"><use href="#i-ic24"/></svg>${o.date} · 经手人 ${o.by}</div>
<div class="mc-foot"><svg width="13" height="13" viewBox="0 0 24 24" stroke-width="1.9" style="color:var(--muted)"><use href="#i-ic24"/></svg>${o.date} · 经手人 ${o.by}${o.auditor?' · 审核 '+o.auditor:''}</div>
</a>`).join('');
}
function setSt(s){ state.status=s; render(); }
function actions(st){
if(st==='待审核') return '<div class="btn danger" style="flex:1;justify-content:center" onclick="mToast(\'已驳回\');closeSheet()">驳回</div><div class="btn primary" style="flex:1;justify-content:center" onclick="mToast(\'已审核通过 ✓\');closeSheet()">通过</div>';
if(st==='已驳回') return '<div class="btn primary" style="flex:1;justify-content:center" onclick="mToast(\'已重新提交 ✓\');closeSheet()">重新提交</div>';
if(st==='已通过') return '<div class="btn ghost" style="flex:1;justify-content:center" onclick="mToast(\'已退单\');closeSheet()">退单</div>';
/* ==================== 详细搜索(底部 sheet,竖排单列) ==================== */
let advOpen='';
function advInput(key,label,ph){ return `<div class="m-field"><label>${label}</label><input class="m-input" value="${adv[key]}" placeholder="${ph}" oninput="adv.${key}=this.value"></div>`; }
function advSelect(key,label,opts){
const open=advOpen===key;
const optRows=open?`<div style="border:1px solid var(--border);border-top:none;border-radius:0 0 var(--r-md) var(--r-md);padding:0 12px;background:var(--surface)">${opts.map(v=>`<div class="m-opt ${v===adv[key]?'sel':''}" onclick="advPick('${key}','${v}')">${v}<svg class="ck" viewBox="0 0 24 24"><use href="#i-check"/></svg></div>`).join('')}</div>`:'';
return `<div class="m-field"><label>${label}</label><div class="m-select" ${open?'style="border-radius:var(--r-md) var(--r-md) 0 0"':''} onclick="advToggle('${key}')"><span class="${adv[key]==='全部'||adv[key]==='全部时间'?'ph':''}">${adv[key]}</span><svg viewBox="0 0 24 24"><use href="#i-chevron-down"/></svg></div>${optRows}</div>`;
}
function advToggle(k){ advOpen=advOpen===k?'':k; drawAdv(); }
function advPick(k,v){ adv[k]=v; advOpen=''; drawAdv(); }
function drawAdv(){
openSheet('详细搜索',
advInput('no','单据编号','输入单据编号')
+advSelect('party','客户',['全部','城东烟酒行','宴汇大酒店','金樽会所','喜宴婚庆'])
+advInput('product','商品信息','酒名 / 商品编码')
+advSelect('series','系列',['全部','飞天','水晶剑','青花','红花郎'])
+advSelect('spec','规格',['全部','53° 500ml×6','52° 500ml×6','42° 480ml×6'])
+advInput('batch','批次 · 编号 · 物流码','批次 / 商品编码 / 物流码')
+advSelect('status','状态',ST)
+advSelect('op','出库员',OPS)
+advSelect('reviewer','审核人',REVIEWERS)
+advSelect('date','单据日期',DATES)
+`<div style="display:flex;gap:10px;margin-top:16px"><div class="btn ghost" style="flex:1;justify-content:center" onclick="resetAdv()">重置</div><div class="btn primary" style="flex:2;justify-content:center" onclick="applyAdv()">确认</div></div>`);
}
function openAdv(){ advOpen=''; drawAdv(); }
function resetAdv(){ adv={...ADV_DEF}; advOpen=''; drawAdv(); }
function applyAdv(){ closeSheet(); render(); }
/* ==================== 单据详情 / 审核 / 确认售价 / 退单 ==================== */
function actions(o){
const st=o.status;
if(st==='草稿') return '<div class="btn primary" style="flex:1;justify-content:center" onclick="mToast(\'已提交审核 ✓\');closeSheet()">提交审核</div>';
if(st==='待审核') return '<div class="btn danger" style="flex:1;justify-content:center" onclick="mToast(\'已拒绝\');closeSheet()">拒绝</div><div class="btn primary" style="flex:1;justify-content:center" onclick="mToast(\'已审核通过 ✓\');closeSheet()">通过</div>';
if(st==='已拒绝') return '<div class="btn primary" style="flex:1;justify-content:center" onclick="mToast(\'已重新提交 ✓\');closeSheet()">重新提交</div>';
if(st==='已审核'&&o.ret!=='已退单') return `<div class="btn danger" style="flex:1;justify-content:center" onclick="openReturn('${o.no}')">退单</div><div class="btn ghost" style="flex:1;justify-content:center" onclick="closeSheet()">关闭</div>`;
return '<div class="btn ghost" style="flex:1;justify-content:center" onclick="closeSheet()">关闭</div>';
}
function openOrder(no){ const o=ORDERS.find(x=>x.no===no);
const pendRow=o.pending?`<div class="drow"><span>售价</span><b><span class="badge b-待定价">含待定价</span></b></div>`:'';
const confirmBtn=(o.pending&&o.status==='已审核')?`<div class="btn primary" style="flex:1;justify-content:center" onclick="openConfirm('${o.no}')"><svg viewBox="0 0 24 24"><use href="#i-edit"/></svg>确认售价</div>`:'';
const lns=o.lines||[];
const lineRows=lns.map((l,i)=>`<div class="drow"${i===lns.length-1?' style="border-bottom:none"':''}><span style="color:var(--text);line-height:1.45">${l.n}<br><span style="color:var(--faint);font-size:var(--fs-xs)">${l.s} · ×${l.q}</span></span><b style="font-family:var(--font-mono)">${l.a}</b></div>`).join('');
openSheet('出库单详情', `<div class="drow"><span>单号</span><b style="font-family:var(--font-mono)">${o.no}</b></div><div class="drow"><span>客户</span><b>${o.party}</b></div><div class="drow"><span>单据日期</span><b>${o.date}</b></div><div class="drow"><span>合计金额</span><b style="font-family:var(--font-mono)">${o.amount}</b></div><div class="drow" style="border-bottom:none"><span>状态</span><b><span class="badge b-${o.status}">${o.status}</span></b></div>
const lineRows=lns.map((l,i)=>`<div class="drow"${i===lns.length-1?' style="border-bottom:none"':''}><span style="color:var(--text);line-height:1.45">${l.n}<br><span style="color:var(--faint);font-size:var(--fs-xs)">${l.s} · ×${l.q}</span></span><b style="font-family:var(--font-mono)">${l.pend?'<span class="badge b-待定价">待定价</span>':l.a}</b></div>`).join('');
openSheet('出库单详情', `<div class="drow"><span>单号</span><b style="font-family:var(--font-mono)">${o.no}</b></div><div class="drow"><span>客户</span><b>${o.party}</b></div><div class="drow"><span>单据日期</span><b>${o.date}</b></div><div class="drow"><span>出库员</span><b>${o.by}</b></div><div class="drow"><span>审核员</span><b>${o.auditor||'—'}</b></div><div class="drow"><span>合计金额</span><b style="font-family:var(--font-mono)">${o.amount}</b></div><div class="drow"><span>合计利润 · 管理员可见</span><b style="font-family:var(--font-mono);color:var(--success)">${o.profit}</b></div>${pendRow}<div class="drow" style="border-bottom:none"><span>状态</span><b><span class="badge b-${o.status}">${o.status}</span>${o.ret?` <span class="badge b-${o.ret}">${o.ret}</span>`:''}</b></div>
<div class="pe-card" style="margin-top:14px"><div class="pe-h"><span class="pe-ic"><svg viewBox="0 0 24 24"><use href="#i-ic33"/></svg></span>商品明细 · ${lns.length} 项</div><div class="pe-b" style="padding-top:2px;padding-bottom:2px">${lineRows}</div></div>
<div style="display:flex;gap:10px;margin-top:16px">${actions(o.status)}</div>`);
${o.pending?'<div style="margin-top:12px;font-size:var(--fs-sm);line-height:1.6;color:var(--warn)">「先卖后定价」:售价待定,价格确定后点「确认售价」按售价×数量结算应收。</div>':''}
<div style="display:flex;gap:10px;margin-top:16px">${confirmBtn}${actions(o)}</div>`);
}
/* ---- 确认售价(对齐桌面 ovConfirmSale,简化为 sheet ---- */
function openConfirm(no){ const o=ORDERS.find(x=>x.no===no); const pend=(o.lines||[]).filter(l=>l.pend);
openSheet('确认售价', `<div class="notice info" style="margin-bottom:14px"><svg viewBox="0 0 24 24"><use href="#i-ic42"/></svg><div>「先卖后定价」:填写真实售价后,系统按售价×数量结算应收并补应收流水——不反审核、不影响成本 / 应付。</div></div>`
+pend.map(l=>`<div class="m-field"><label>${l.n} · 数量 ${l.q}</label><input class="m-input" type="number" inputmode="decimal" placeholder="真实售价 0.00"></div>`).join('')
+`<div style="display:flex;gap:10px;margin-top:16px"><div class="btn ghost" style="flex:1;justify-content:center" onclick="closeSheet()">取消</div><div class="btn primary" style="flex:2;justify-content:center" onclick="mToast('售价已确认,已按售价×数量结算应收并补应收流水 ✓');closeSheet()">确认并补应收</div></div>`);
}
/* ---- 退单(对齐桌面 ovReturn:勾选明细 + 数量,出库=加回库存 +) ---- */
let retNo='', retStage={};
function openReturn(no){ retNo=no; retStage={}; drawReturn(); }
function toggleRet(i,max){ if(retStage[i]!=null) delete retStage[i]; else retStage[i]=max; drawReturn(); }
function setRetQty(i,v,max){ retStage[i]=Math.min(Math.max(1,Math.floor(+v||0)),max); drawReturnSummary(); }
function stageAllRet(){ const o=ORDERS.find(x=>x.no===retNo); (o.lines||[]).forEach((l,i)=>retStage[i]=l.q); drawReturn(); }
function drawReturnSummary(){ const el=document.getElementById('retSum'); if(!el)return; const keys=Object.keys(retStage); const qty=keys.reduce((s,k)=>s+(+retStage[k]||0),0); el.textContent=keys.length?`已退单 ${keys.length} 项 · 退回库存 +${qty}`:'未选择退单明细'; }
function drawReturn(){ const o=ORDERS.find(x=>x.no===retNo);
openSheet('退单', `<div class="notice info" style="margin-bottom:14px"><svg viewBox="0 0 24 24"><use href="#i-ic42"/></svg><div>提交后,退单明细的数量将「加回库存」。退单明细不会删除,以红色「已退单」标注,且不可撤销。</div></div>`
+(o.lines||[]).map((l,i)=>{ const st=retStage[i]!=null;
return `<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px"><label style="display:flex;align-items:center;gap:9px;flex:1;cursor:pointer"><input type="checkbox" ${st?'checked':''} onchange="toggleRet(${i},${l.q})"><span><div style="font-weight:600;color:var(--heading);font-size:var(--fs-body)">${l.n}</div><div style="color:var(--faint);font-size:var(--fs-xs)">${l.s} · 数量 ${l.q}</div></span></label><input class="m-input" style="width:104px" type="number" min="1" max="${l.q}" value="${st?retStage[i]:''}" ${st?'':'disabled'} placeholder="退货数量" oninput="setRetQty(${i},this.value,${l.q})"></div>`; }).join('')
+`<div style="display:flex;align-items:center;gap:10px;margin-top:8px"><div class="btn ghost sm" onclick="stageAllRet()">全部退单</div><span id="retSum" style="font-size:var(--fs-sm);color:var(--muted)">未选择退单明细</span></div>
<div style="display:flex;gap:10px;margin-top:16px"><div class="btn ghost" style="flex:1;justify-content:center" onclick="closeSheet()">取消</div><div class="btn danger" style="flex:2;justify-content:center" onclick="submitReturn()">提交退单</div></div>`);
drawReturnSummary();
}
function submitReturn(){ const keys=Object.keys(retStage); if(!keys.length){mToast('请先选择要退的明细');return;} const o=ORDERS.find(x=>x.no===retNo); o.ret=keys.length<(o.lines||[]).length?'部分退单':'已退单'; closeSheet(); render(); mToast('退单成功 ✓'); }
render();
</script>
</body>