fix: 子卷包装关系新增判断erp卷号是否存在

This commit is contained in:
2026-08-18 13:57:14 +08:00
parent 68a7112921
commit d3feb61e62
5 changed files with 260 additions and 73 deletions

View File

@@ -0,0 +1,85 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>批量新增子卷包装关系 · 卡片式方案</title>
<style>
:root{--blue:#2f6bff;--blue2:#eef4ff;--ink:#172033;--muted:#737b8c;--line:#e7eaf0;--bg:#f4f6fa;--white:#fff;--red:#e5484d;--green:#21a366;--shadow:0 18px 60px rgba(20,36,72,.18)}
*{box-sizing:border-box}html,body{height:100%;margin:0;font:14px/1.5 "Microsoft YaHei","Segoe UI",sans-serif;color:var(--ink);background:var(--bg)}button,input,select{font:inherit}button{cursor:pointer}.layout{display:grid;grid-template-columns:220px 1fr;height:100%;overflow:hidden}.side{background:#17233b;color:#c9d2e3}.logo{height:60px;display:flex;align-items:center;gap:11px;padding:0 20px;color:#fff;font-size:16px;font-weight:700}.logo-badge{width:30px;height:30px;border-radius:9px;background:linear-gradient(135deg,#63a7ff,#2f6bff);display:grid;place-items:center;font-size:12px}.nav{padding:12px}.nav-label{font-size:12px;color:#697996;padding:12px 10px 7px}.nav-item{padding:11px 13px;border-radius:8px;margin:3px 0}.nav-item.active{background:#2f6bff;color:#fff}.workspace{min-width:0;display:flex;flex-direction:column}.top{height:60px;background:#fff;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 22px}.top-right{display:flex;align-items:center;gap:18px}.avatar{width:30px;height:30px;border-radius:50%;display:grid;place-items:center;background:#eaf1ff;color:var(--blue);font-weight:700}.page{padding:18px;overflow:auto}.crumb{font-size:13px;color:#8a91a1;margin-bottom:14px}.panel{background:#fff;border:1px solid var(--line);border-radius:10px;padding:18px}.filter-row{display:grid;grid-template-columns:repeat(4,1fr);gap:18px}.field-line{display:flex;align-items:center;gap:9px}.field-line label{white-space:nowrap}.control{height:34px;width:100%;border:1px solid #d9dde7;border-radius:7px;padding:0 10px;background:#fff;outline:none}.control:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(47,107,255,.12)}.actions{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}.btn{border:1px solid #d9dde7;background:#fff;color:#344054;height:34px;border-radius:7px;padding:0 14px;display:inline-flex;align-items:center;justify-content:center;gap:6px}.btn:hover{border-color:var(--blue);color:var(--blue)}.btn.primary{background:var(--blue);border-color:var(--blue);color:#fff}.btn.ghost-blue{background:var(--blue2);border-color:#cfe0ff;color:var(--blue)}.btn.danger{color:var(--red)}.list-panel{margin-top:14px}.list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:15px}.list-title{font-size:17px;font-weight:700}.fake-table{width:100%;border-collapse:collapse}.fake-table th{background:#f7f8fa;text-align:left;color:#667085;padding:11px 12px}.fake-table td{padding:13px 12px;border-bottom:1px solid #f0f1f4}.pill{display:inline-flex;padding:2px 9px;border-radius:20px;background:#eaf8f1;color:#198754;font-size:12px}
.overlay{position:fixed;inset:0;background:rgba(18,25,38,.52);z-index:20;display:grid;place-items:center;padding:24px}.overlay.hidden{display:none}.modal{width:min(1180px,calc(100vw - 48px));height:min(790px,calc(100vh - 50px));background:#f7f9fc;border-radius:14px;box-shadow:var(--shadow);display:flex;flex-direction:column;overflow:hidden}.modal-head{height:68px;background:#fff;border-bottom:1px solid var(--line);padding:0 24px;display:flex;align-items:center;justify-content:space-between}.modal-title{display:flex;align-items:center;gap:12px}.title-icon{width:38px;height:38px;border-radius:10px;background:var(--blue2);color:var(--blue);display:grid;place-items:center;font-size:20px}.modal-title h2{font-size:18px;margin:0}.modal-title p{margin:2px 0 0;color:var(--muted);font-size:12px}.icon-btn{width:34px;height:34px;border:0;border-radius:8px;background:transparent;color:#8a91a1;font-size:22px}.icon-btn:hover{background:#f0f2f5}.command{height:60px;background:#fff;padding:12px 24px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line)}.command-left{display:flex;gap:9px}.counter{color:var(--muted)}.counter strong{font-size:18px;color:var(--blue);margin:0 3px}.modal-body{flex:1;min-height:0;overflow:auto;padding:16px 24px 22px}.notice{border-radius:9px;background:#fff7e8;border:1px solid #ffe0a3;color:#8a5a00;padding:10px 13px;margin-bottom:13px;display:none}.notice.show{display:block}.record{background:#fff;border:1px solid var(--line);border-radius:12px;margin-bottom:14px;box-shadow:0 2px 8px rgba(23,32,51,.04);overflow:hidden}.record.invalid{border-color:#f2a7aa}.record-head{height:48px;background:linear-gradient(90deg,#f8faff,#fff);border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 16px}.record-no{display:flex;align-items:center;gap:9px;font-weight:700}.number{width:26px;height:26px;border-radius:8px;background:var(--blue);color:#fff;display:grid;place-items:center;font-size:12px}.record-actions{display:flex;gap:3px}.text-btn{border:0;background:transparent;color:var(--blue);padding:6px 9px;border-radius:6px}.text-btn:hover{background:var(--blue2)}.text-btn.delete{color:var(--red)}.record-body{padding:15px 16px 17px}.section{display:grid;grid-template-columns:125px 1fr;gap:14px;align-items:start}.section+.section{border-top:1px dashed #e3e7ef;margin-top:14px;padding-top:14px}.section-label{display:flex;align-items:center;gap:7px;color:#576071;font-weight:700;padding-top:8px}.section-dot{width:7px;height:7px;border-radius:50%;background:#84a9ff}.grid{display:grid;gap:13px}.grid.identity{grid-template-columns:1.2fr 1.2fr .8fr}.grid.spec{grid-template-columns:1.15fr repeat(3,.75fr)}.grid.lifecycle{grid-template-columns:1fr 1fr 1fr}.form-item label{display:flex;gap:3px;color:#596275;font-size:12px;margin-bottom:5px}.req{color:var(--red)}.form-item input,.form-item select{height:36px;width:100%;border:1px solid #d9dde7;border-radius:7px;padding:0 10px;background:#fff;outline:none}.form-item input:focus,.form-item select:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(47,107,255,.1)}.form-item input[readonly]{background:#f2f4f7;color:#667085}.with-unit{position:relative}.with-unit input{padding-right:36px}.unit{position:absolute;right:10px;bottom:9px;color:#98a0af;font-size:12px}.form-item.error input,.form-item.error select{border-color:var(--red);background:#fff8f8}.error-msg{display:none;color:var(--red);font-size:11px;margin-top:3px}.form-item.error .error-msg{display:block}.modal-foot{height:68px;background:#fff;border-top:1px solid var(--line);padding:0 24px;display:flex;align-items:center;justify-content:space-between}.foot-note{font-size:12px;color:var(--muted)}.foot-actions{display:flex;gap:10px}.foot-actions .btn{height:38px;min-width:96px}.confirm-layer{position:absolute;inset:0;background:rgba(16,24,40,.28);display:grid;place-items:center;z-index:5}.confirm-layer.hidden{display:none}.confirm-box{width:390px;background:#fff;border-radius:12px;padding:22px;box-shadow:var(--shadow)}.confirm-box h3{margin:0 0 8px}.confirm-box p{margin:0;color:var(--muted)}.confirm-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:22px}.toast{position:fixed;top:22px;left:50%;transform:translateX(-50%) translateY(-10px);padding:11px 18px;background:#fff;border-radius:9px;box-shadow:0 8px 30px rgba(0,0,0,.18);z-index:50;opacity:0;transition:.2s;pointer-events:none}.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}.toast span{color:var(--green);font-weight:700;margin-right:7px}
.table-modal{width:min(1360px,calc(100vw - 40px));height:min(740px,calc(100vh - 48px));background:#fff}.table-modal .modal-body{padding:14px 20px 18px;background:#f7f9fc}.table-tip{display:flex;align-items:center;justify-content:space-between;background:#eef5ff;border:1px solid #cfe0ff;color:#315b9d;border-radius:8px;padding:9px 12px;margin-bottom:12px}.edit-wrap{height:100%;background:#fff;border:1px solid #dfe3ea;border-radius:9px;overflow:auto}.edit-table{border-collapse:separate;border-spacing:0;min-width:1780px;width:100%;font-size:13px}.edit-table th,.edit-table td{border-right:1px solid #e5e8ee;border-bottom:1px solid #e5e8ee;padding:7px;background:#fff;vertical-align:top}.edit-table thead th{position:sticky;top:0;z-index:3;height:44px;background:#f3f6fb;color:#4b5870;text-align:left;vertical-align:middle}.edit-table .fix-left{position:sticky;left:0;z-index:2}.edit-table .fix-right{position:sticky;right:0;z-index:2;box-shadow:-5px 0 12px rgba(21,35,62,.06)}.edit-table thead .fix-left,.edit-table thead .fix-right{z-index:5}.edit-table td.fix-left,.edit-table td.fix-right{background:#fff}.edit-table .index{width:54px;text-align:center;color:#788196}.edit-cell{min-height:48px}.edit-cell input,.edit-cell select{width:100%;height:34px;border:1px solid #d9dde7;border-radius:6px;padding:0 9px;outline:none;background:#fff}.edit-cell input:focus,.edit-cell select:focus{border-color:var(--blue);box-shadow:0 0 0 2px rgba(47,107,255,.1)}.edit-cell input[readonly]{background:#f2f4f7;color:#667085}.edit-cell.bad input,.edit-cell.bad select{border-color:var(--red);background:#fff8f8}.cell-error{display:none;color:var(--red);font-size:10px;margin-top:2px}.edit-cell.bad .cell-error{display:block}.table-row-actions{height:34px;display:flex;align-items:center;justify-content:center;gap:2px;white-space:nowrap}.th-req:before{content:"*";color:var(--red);margin-right:3px}.table-notice{display:none;color:#b54708;background:#fff7e8;border:1px solid #ffdca0;border-radius:7px;padding:7px 10px;margin-bottom:10px}.table-notice.show{display:block}.view-switch{display:flex;background:#f0f2f5;border-radius:8px;padding:3px;gap:2px}.view-btn{height:30px;padding:0 14px;border:0;border-radius:6px;background:transparent;color:#667085}.view-btn.active{background:#fff;color:var(--blue);box-shadow:0 1px 4px rgba(30,45,75,.14);font-weight:700}.view-hidden{display:none!important}.hybrid-cards{height:100%;overflow:auto;padding-right:3px}.hybrid-cards .record{margin-bottom:12px}.hybrid-cards .record-body{padding-top:13px;padding-bottom:14px}@media(max-width:1000px){.layout{grid-template-columns:68px 1fr}.side .logo-name,.nav-item span,.nav-label{display:none}.logo{justify-content:center;padding:0}.nav-item{text-align:center}.filter-row{grid-template-columns:repeat(2,1fr)}.section{grid-template-columns:1fr}.grid.identity,.grid.spec,.grid.lifecycle{grid-template-columns:repeat(2,1fr)}}@media(prefers-reduced-motion:reduce){*{transition:none!important}}
</style>
</head>
<body>
<div class="layout"><aside class="side"><div class="logo"><div class="logo-badge">NL</div><span class="logo-name">华创智能制造</span></div><nav class="nav"><div class="nav-label">物流管理</div><div class="nav-item">▦ <span>包装点管理</span></div><div class="nav-item active">▣ <span>子卷包装关系</span></div><div class="nav-item">▤ <span>成品入库</span></div><div class="nav-label">系统管理</div><div class="nav-item">⚙ <span>基础设置</span></div></nav></aside><div class="workspace"><header class="top"><span></span><div class="top-right"><span></span><span>消息</span><span>管理员</span><div class="avatar"></div></div></header><main class="page"><div class="crumb">首页 / 物流管理 / 子卷包装关系</div><section class="panel"><div class="filter-row"><div class="field-line"><label>木箱唯一码</label><input class="control" placeholder="请输入木箱唯一码"></div><div class="field-line"><label>木箱类型</label><select class="control"><option>全部</option><option>BX-A</option><option>BX-B</option></select></div><div class="field-line"><label>子卷号</label><input class="control" placeholder="请输入子卷号"></div><div class="field-line"><label>状态</label><select class="control"><option>全部</option><option>待包装</option><option>已包装</option></select></div></div><div class="actions"><button class="btn">重置</button><button class="btn primary">搜索</button></div></section><section class="panel list-panel"><div class="list-head"><div class="list-title">子卷包装关系列表</div><div><button class="btn primary" id="openBtn"> 批量新增</button> <button class="btn ghost-blue" id="openTableBtn">▦ 表格批量新增</button> <button class="btn ghost-blue" id="openHybridBtn">⇄ 自由切换</button> <button class="btn">导出</button></div></div><table class="fake-table"><thead><tr><th>木箱唯一码</th><th>木箱类型</th><th>尺寸(mm)</th><th>子卷号</th><th>入库日期</th><th>状态</th></tr></thead><tbody id="listBody"><tr><td>BXSN-260817-001</td><td>BX-A</td><td>1200 × 1000 × 900</td><td>JR-260817-01</td><td>2026-08-17</td><td><span class="pill">已入库</span></td></tr><tr><td>BXSN-260817-002</td><td>BX-B</td><td>1100 × 900 × 850</td><td>JR-260817-02</td><td>2026-08-17</td><td><span class="pill">已包装</span></td></tr></tbody></table></section></main></div></div>
<div class="overlay" id="modal" role="dialog" aria-modal="true" aria-labelledby="title"><section class="modal"><header class="modal-head"><div class="modal-title"><div class="title-icon"></div><div><h2 id="title">批量新增子卷包装关系</h2><p>逐条录入木箱、子卷及入库信息,尺寸由木箱类型自动匹配</p></div></div><button class="icon-btn" id="closeBtn" aria-label="关闭">×</button></header><div class="command"><div class="command-left"><button class="btn primary" id="addBtn"> 新增一行</button><button class="btn ghost-blue" id="clearBtn">⌫ 清空全部</button></div><div class="counter">已添加 <strong id="count">2</strong> 条记录</div></div><main class="modal-body" id="scroll"><div class="notice" id="notice">请检查标红字段后再提交。</div><div id="records"></div></main><footer class="modal-foot"><div class="foot-note"><span class="req">*</span> 为必填项;所有数据仅用于原型演示</div><div class="foot-actions"><button class="btn" id="cancelBtn">取消</button><button class="btn primary" id="submitBtn">确认新增2 条)</button></div></footer></section><div class="confirm-layer hidden" id="confirmLayer"><div class="confirm-box"><h3 id="confirmTitle">确认操作</h3><p id="confirmText"></p><div class="confirm-buttons"><button class="btn" id="confirmCancel">取消</button><button class="btn primary" id="confirmOk">确认</button></div></div></div></div><div class="toast" id="toast"><span></span><b id="toastText"></b></div>
<div class="overlay hidden" id="tableModal" role="dialog" aria-modal="true" aria-labelledby="tableTitle"><section class="modal table-modal"><header class="modal-head"><div class="modal-title"><div class="title-icon"></div><div><h2 id="tableTitle">自由切换批量新增</h2><p>卡片适合逐条录入,表格适合批量核对;切换时数据自动保留</p></div></div><button class="icon-btn" id="tableCloseBtn" aria-label="关闭自由切换批量新增">×</button></header><div class="command"><div class="command-left"><button class="btn primary" id="tableAddBtn"> 新增一行</button><button class="btn ghost-blue" id="tableClearBtn">⌫ 清空全部</button></div><div class="view-switch" aria-label="录入视图"><button class="view-btn" id="cardViewBtn">▤ 卡片</button><button class="view-btn active" id="tableViewBtn">▦ 表格</button></div><div class="counter">当前共 <strong id="tableCount">3</strong> 条记录</div></div><main class="modal-body"><div class="table-notice" id="tableNotice"></div><div class="table-tip"><span id="viewTip">选择木箱类型后自动带出长、宽、高;复制后请补充新的唯一码和子卷号。</span><span>尺寸单位mm</span></div><div class="hybrid-cards view-hidden" id="hybridCards"></div><div class="edit-wrap" id="hybridTable"><table class="edit-table"><thead><tr><th class="index fix-left">序号</th><th class="th-req" style="min-width:190px">木箱唯一码</th><th class="th-req" style="min-width:180px">木箱类型</th><th style="min-width:120px">木箱长</th><th style="min-width:120px">木箱宽</th><th style="min-width:120px">木箱高</th><th class="th-req" style="min-width:140px">保质期(天)</th><th class="th-req" style="min-width:150px">入库日期</th><th class="th-req" style="min-width:190px">子卷号</th><th class="th-req" style="min-width:140px">状态</th><th class="fix-right" style="min-width:116px;text-align:center">操作</th></tr></thead><tbody id="tableBody"></tbody></table></div></main><footer class="modal-foot"><div class="foot-note"><span class="req">*</span> 为必填项;数据仅用于原型演示</div><div class="foot-actions"><button class="btn" id="tableCancelBtn">取消</button><button class="btn primary" id="tableSubmitBtn">确认新增3 条)</button></div></footer></section><div class="confirm-layer hidden" id="tableConfirmLayer"><div class="confirm-box"><h3 id="tableConfirmTitle">确认操作</h3><p id="tableConfirmText"></p><div class="confirm-buttons"><button class="btn" id="tableConfirmCancel">取消</button><button class="btn primary" id="tableConfirmOk">确认</button></div></div></div></div>
<script>
const boxTypes={'BX-A 标准出口箱':{length:1200,width:1000,high:900},'BX-B 加固木箱':{length:1100,width:900,high:850},'BX-C 小型木箱':{length:800,width:600,high:600}};
const statusOptions=['待包装','已包装','已入库','已作废'];
let seq=1,rows=[],errors=[],pending=null;
const $=id=>document.getElementById(id),fields=['package_box_sn','box_type','quality_guaran_period','date_of_FG_inbound','container_name','status'];
const empty=()=>({key:seq++,package_box_sn:'',box_type:'',box_length:'',box_width:'',box_high:'',quality_guaran_period:'',date_of_FG_inbound:'',container_name:'',status:'待包装'});
const esc=v=>String(v??'').replace(/[&<>"]/g,x=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[x]));
const error=(key,field)=>errors.find(e=>e.key===key&&e.field===field);
function item(r,field,label,control){const e=error(r.key,field);return `<div class="form-item ${e?'error':''}" data-item="${r.key}-${field}"><label><span class="req">*</span>${label}</label>${control}<div class="error-msg">${e?esc(e.msg):''}</div></div>`}
function render(){
$('records').innerHTML=rows.map((r,i)=>`<article class="record ${errors.some(e=>e.key===r.key)?'invalid':''}"><header class="record-head"><div class="record-no"><span class="number">${i+1}</span>包装关系 ${String(i+1).padStart(2,'0')}</div><div class="record-actions"><button class="text-btn" data-act="copy" data-key="${r.key}">⧉ 复制</button><button class="text-btn delete" data-act="delete" data-key="${r.key}">⌫ 删除</button></div></header><div class="record-body"><section class="section"><div class="section-label"><span class="section-dot"></span>识别信息</div><div class="grid identity">${item(r,'package_box_sn','木箱唯一码',`<input data-key="${r.key}" data-field="package_box_sn" value="${esc(r.package_box_sn)}" placeholder="扫描或输入木箱唯一码">`)}${item(r,'container_name','子卷号',`<input data-key="${r.key}" data-field="container_name" value="${esc(r.container_name)}" placeholder="扫描或输入子卷号">`)}${item(r,'status','状态',`<select data-key="${r.key}" data-field="status">${statusOptions.map(s=>`<option ${r.status===s?'selected':''}>${s}</option>`).join('')}</select>`)}</div></section><section class="section"><div class="section-label"><span class="section-dot"></span>木箱规格</div><div class="grid spec">${item(r,'box_type','木箱类型',`<select data-key="${r.key}" data-field="box_type"><option value="">请选择木箱类型</option>${Object.keys(boxTypes).map(s=>`<option ${r.box_type===s?'selected':''}>${s}</option>`).join('')}</select>`)}<div class="form-item"><label>木箱长</label><div class="with-unit"><input readonly value="${esc(r.box_length)}" placeholder="自动带出"><span class="unit">mm</span></div></div><div class="form-item"><label>木箱宽</label><div class="with-unit"><input readonly value="${esc(r.box_width)}" placeholder="自动带出"><span class="unit">mm</span></div></div><div class="form-item"><label>木箱高</label><div class="with-unit"><input readonly value="${esc(r.box_high)}" placeholder="自动带出"><span class="unit">mm</span></div></div></div></section><section class="section"><div class="section-label"><span class="section-dot"></span>时效信息</div><div class="grid lifecycle">${item(r,'quality_guaran_period','保质期',`<div class="with-unit"><input type="number" min="1" data-key="${r.key}" data-field="quality_guaran_period" value="${esc(r.quality_guaran_period)}" placeholder="例如90"><span class="unit">天</span></div>`)}${item(r,'date_of_FG_inbound','入库日期',`<input type="date" data-key="${r.key}" data-field="date_of_FG_inbound" value="${esc(r.date_of_FG_inbound)}">`)}<div></div></div></section></div></article>`).join('');
$('count').textContent=rows.length;$('submitBtn').textContent=`确认新增(${rows.length} 条)`;
}
function reset(){rows=[empty(),empty()];errors=[];$('notice').classList.remove('show');render()}
$('records').addEventListener('input',e=>{const k=Number(e.target.dataset.key),f=e.target.dataset.field;if(!k||!f)return;rows.find(r=>r.key===k)[f]=e.target.value;errors=errors.filter(x=>!(x.key===k&&x.field===f));e.target.closest('.form-item')?.classList.remove('error')});
$('records').addEventListener('change',e=>{const k=Number(e.target.dataset.key),f=e.target.dataset.field;if(f!=='box_type')return;const r=rows.find(x=>x.key===k),d=boxTypes[e.target.value];r.box_type=e.target.value;r.box_length=d?.length||'';r.box_width=d?.width||'';r.box_high=d?.high||'';render()});
$('records').addEventListener('click',e=>{const b=e.target.closest('[data-act]');if(!b)return;const i=rows.findIndex(r=>r.key===Number(b.dataset.key));if(b.dataset.act==='copy'){const n={...rows[i],key:seq++,package_box_sn:'',container_name:''};rows.splice(i+1,0,n);errors=[];render();toast('已复制记录,请补充唯一码和子卷号')}else{rows.splice(i,1);if(!rows.length)rows=[empty()];errors=[];render()}});
$('addBtn').onclick=()=>{rows.push(empty());render();$('records').lastElementChild.scrollIntoView({behavior:'smooth',block:'center'})};
function ask(title,text,fn){$('confirmTitle').textContent=title;$('confirmText').textContent=text;pending=fn;$('confirmLayer').classList.remove('hidden')}
$('clearBtn').onclick=()=>ask('清空全部记录','已输入的数据将被清空,并恢复为 2 条空白记录。',reset);$('confirmCancel').onclick=()=>{$('confirmLayer').classList.add('hidden');pending=null};$('confirmOk').onclick=()=>{const fn=pending;$('confirmLayer').classList.add('hidden');pending=null;fn?.()};
const hasData=()=>rows.some(r=>fields.some(f=>f!=='status'&&String(r[f]||'').trim()));
function close(){ $('modal').classList.add('hidden') }
function requestClose(){hasData()?ask('放弃本次录入?','关闭后当前未提交内容不会保留。',()=>{close();reset()}):close()}
$('cancelBtn').onclick=requestClose;$('closeBtn').onclick=requestClose;$('openBtn').onclick=()=>{reset();$('modal').classList.remove('hidden')};
function validate(){errors=[];rows.forEach(r=>{fields.forEach(f=>{if(!String(r[f]??'').trim())errors.push({key:r.key,field:f,msg:'此项为必填'})});if(r.quality_guaran_period&&Number(r.quality_guaran_period)<=0)errors.push({key:r.key,field:'quality_guaran_period',msg:'必须大于 0 天'})});['package_box_sn','container_name'].forEach(f=>{const seen=new Map();rows.forEach((r,i)=>{const v=String(r[f]||'').trim();if(!v)return;if(seen.has(v)){const j=seen.get(v);errors.push({key:r.key,field:f,msg:`与第 ${j+1} 条重复`});errors.push({key:rows[j].key,field:f,msg:`与第 ${i+1} 条重复`})}else seen.set(v,i)})});render();if(errors.length){$('notice').textContent=`发现 ${errors.length} 处问题,请检查标红字段。`;$('notice').classList.add('show');document.querySelector(`[data-item="${errors[0].key}-${errors[0].field}"]`)?.scrollIntoView({behavior:'smooth',block:'center'});return false}$('notice').classList.remove('show');return true}
$('submitBtn').onclick=()=>{if(!validate())return;const b=$('submitBtn'),n=rows.length;b.disabled=true;b.textContent='正在提交…';setTimeout(()=>{const r=rows[0];$('listBody').insertAdjacentHTML('afterbegin',`<tr><td>${esc(r.package_box_sn)}</td><td>${esc(r.box_type.split(' ')[0])}</td><td>${r.box_length} × ${r.box_width} × ${r.box_high}</td><td>${esc(r.container_name)}</td><td>${esc(r.date_of_FG_inbound)}</td><td><span class="pill">${esc(r.status)}</span></td></tr>`);b.disabled=false;close();toast(`成功新增 ${n} 条子卷包装关系`);reset()},600)};
function toast(t){$('toastText').textContent=t;$('toast').classList.add('show');setTimeout(()=>$('toast').classList.remove('show'),2200)}
let tableSeq=1,tableRows=[],tableErrors=[],tablePending=null,hybridView='table';
const tableRequired=['package_box_sn','box_type','quality_guaran_period','date_of_FG_inbound','container_name','status'];
const tableEmpty=()=>({key:tableSeq++,package_box_sn:'',box_type:'',box_length:'',box_width:'',box_high:'',quality_guaran_period:'',date_of_FG_inbound:'',container_name:'',status:'待包装'});
const tableError=(key,field)=>tableErrors.find(e=>e.key===key&&e.field===field);
function tableCell(r,field,html){const e=tableError(r.key,field);return `<td><div class="edit-cell ${e?'bad':''}" data-table-cell="${r.key}-${field}">${html}<div class="cell-error">${e?esc(e.msg):''}</div></div></td>`}
function hybridItem(r,field,label,control){const e=tableError(r.key,field);return `<div class="form-item ${e?'error':''}" data-hybrid-item="${r.key}-${field}"><label><span class="req">*</span>${label}</label>${control}<div class="error-msg">${e?esc(e.msg):''}</div></div>`}
function renderHybridCards(){
$('hybridCards').innerHTML=tableRows.map((r,i)=>`<article class="record ${tableErrors.some(e=>e.key===r.key)?'invalid':''}"><header class="record-head"><div class="record-no"><span class="number">${i+1}</span>包装关系 ${String(i+1).padStart(2,'0')}</div><div class="record-actions"><button class="text-btn" data-hact="copy" data-hkey="${r.key}">⧉ 复制</button><button class="text-btn delete" data-hact="delete" data-hkey="${r.key}">⌫ 删除</button></div></header><div class="record-body"><section class="section"><div class="section-label"><span class="section-dot"></span>识别信息</div><div class="grid identity">${hybridItem(r,'package_box_sn','木箱唯一码',`<input data-hkey="${r.key}" data-hfield="package_box_sn" value="${esc(r.package_box_sn)}" placeholder="扫描或输入木箱唯一码">`)}${hybridItem(r,'container_name','子卷号',`<input data-hkey="${r.key}" data-hfield="container_name" value="${esc(r.container_name)}" placeholder="扫描或输入子卷号">`)}${hybridItem(r,'status','状态',`<select data-hkey="${r.key}" data-hfield="status">${statusOptions.map(s=>`<option ${r.status===s?'selected':''}>${s}</option>`).join('')}</select>`)}</div></section><section class="section"><div class="section-label"><span class="section-dot"></span>木箱规格</div><div class="grid spec">${hybridItem(r,'box_type','木箱类型',`<select data-hkey="${r.key}" data-hfield="box_type"><option value="">请选择木箱类型</option>${Object.keys(boxTypes).map(s=>`<option ${r.box_type===s?'selected':''}>${s}</option>`).join('')}</select>`)}<div class="form-item"><label>木箱长</label><div class="with-unit"><input readonly value="${esc(r.box_length)}" placeholder="自动带出"><span class="unit">mm</span></div></div><div class="form-item"><label>木箱宽</label><div class="with-unit"><input readonly value="${esc(r.box_width)}" placeholder="自动带出"><span class="unit">mm</span></div></div><div class="form-item"><label>木箱高</label><div class="with-unit"><input readonly value="${esc(r.box_high)}" placeholder="自动带出"><span class="unit">mm</span></div></div></div></section><section class="section"><div class="section-label"><span class="section-dot"></span>时效信息</div><div class="grid lifecycle">${hybridItem(r,'quality_guaran_period','保质期',`<div class="with-unit"><input type="number" min="1" data-hkey="${r.key}" data-hfield="quality_guaran_period" value="${esc(r.quality_guaran_period)}" placeholder="例如90"><span class="unit">天</span></div>`)}${hybridItem(r,'date_of_FG_inbound','入库日期',`<input type="date" data-hkey="${r.key}" data-hfield="date_of_FG_inbound" value="${esc(r.date_of_FG_inbound)}">`)}<div></div></div></section></div></article>`).join('');
}
function renderTableRows(){
$('tableBody').innerHTML=tableRows.map((r,i)=>`<tr><td class="index fix-left">${i+1}</td>${tableCell(r,'package_box_sn',`<input data-tkey="${r.key}" data-tfield="package_box_sn" value="${esc(r.package_box_sn)}" placeholder="请输入木箱唯一码">`)}${tableCell(r,'box_type',`<select data-tkey="${r.key}" data-tfield="box_type"><option value="">请选择木箱类型</option>${Object.keys(boxTypes).map(s=>`<option ${r.box_type===s?'selected':''}>${s}</option>`).join('')}</select>`)}${tableCell(r,'box_length',`<input readonly value="${esc(r.box_length)}" placeholder="自动带出">`)}${tableCell(r,'box_width',`<input readonly value="${esc(r.box_width)}" placeholder="自动带出">`)}${tableCell(r,'box_high',`<input readonly value="${esc(r.box_high)}" placeholder="自动带出">`)}${tableCell(r,'quality_guaran_period',`<input type="number" min="1" data-tkey="${r.key}" data-tfield="quality_guaran_period" value="${esc(r.quality_guaran_period)}" placeholder="例如90">`)}${tableCell(r,'date_of_FG_inbound',`<input type="date" data-tkey="${r.key}" data-tfield="date_of_FG_inbound" value="${esc(r.date_of_FG_inbound)}">`)}${tableCell(r,'container_name',`<input data-tkey="${r.key}" data-tfield="container_name" value="${esc(r.container_name)}" placeholder="请输入子卷号">`)}${tableCell(r,'status',`<select data-tkey="${r.key}" data-tfield="status">${statusOptions.map(s=>`<option ${r.status===s?'selected':''}>${s}</option>`).join('')}</select>`)}<td class="fix-right"><div class="table-row-actions"><button class="text-btn" data-tact="copy" data-tkey="${r.key}">复制</button><button class="text-btn delete" data-tact="delete" data-tkey="${r.key}">删除</button></div></td></tr>`).join('');
renderHybridCards();$('tableCount').textContent=tableRows.length;$('tableSubmitBtn').textContent=`确认新增(${tableRows.length} 条)`;
}
function tableReset(){tableRows=[tableEmpty(),tableEmpty(),tableEmpty()];tableErrors=[];$('tableNotice').classList.remove('show');renderTableRows()}
$('tableBody').addEventListener('input',e=>{const key=Number(e.target.dataset.tkey),field=e.target.dataset.tfield;if(!key||!field)return;tableRows.find(r=>r.key===key)[field]=e.target.value;tableErrors=tableErrors.filter(x=>!(x.key===key&&x.field===field));e.target.closest('.edit-cell')?.classList.remove('bad')});
$('tableBody').addEventListener('change',e=>{const key=Number(e.target.dataset.tkey),field=e.target.dataset.tfield;if(field!=='box_type')return;const r=tableRows.find(x=>x.key===key),d=boxTypes[e.target.value];r.box_type=e.target.value;r.box_length=d?.length||'';r.box_width=d?.width||'';r.box_high=d?.high||'';renderTableRows()});
$('tableBody').addEventListener('click',e=>{const b=e.target.closest('[data-tact]');if(!b)return;const i=tableRows.findIndex(r=>r.key===Number(b.dataset.tkey));if(b.dataset.tact==='copy'){const n={...tableRows[i],key:tableSeq++,package_box_sn:'',container_name:''};tableRows.splice(i+1,0,n);tableErrors=[];renderTableRows();toast('已复制该行,请补充唯一码和子卷号')}else{tableRows.splice(i,1);if(!tableRows.length)tableRows=[tableEmpty()];tableErrors=[];renderTableRows()}});
$('hybridCards').addEventListener('input',e=>{const key=Number(e.target.dataset.hkey),field=e.target.dataset.hfield;if(!key||!field)return;tableRows.find(r=>r.key===key)[field]=e.target.value;tableErrors=tableErrors.filter(x=>!(x.key===key&&x.field===field));e.target.closest('.form-item')?.classList.remove('error')});
$('hybridCards').addEventListener('change',e=>{const key=Number(e.target.dataset.hkey),field=e.target.dataset.hfield;if(field!=='box_type')return;const r=tableRows.find(x=>x.key===key),d=boxTypes[e.target.value];r.box_type=e.target.value;r.box_length=d?.length||'';r.box_width=d?.width||'';r.box_high=d?.high||'';renderTableRows()});
$('hybridCards').addEventListener('click',e=>{const b=e.target.closest('[data-hact]');if(!b)return;const i=tableRows.findIndex(r=>r.key===Number(b.dataset.hkey));if(b.dataset.hact==='copy'){const n={...tableRows[i],key:tableSeq++,package_box_sn:'',container_name:''};tableRows.splice(i+1,0,n);tableErrors=[];renderTableRows();toast('已复制该卡片,请补充唯一码和子卷号')}else{tableRows.splice(i,1);if(!tableRows.length)tableRows=[tableEmpty()];tableErrors=[];renderTableRows()}});
$('tableAddBtn').onclick=()=>{tableRows.push(tableEmpty());renderTableRows();$('tableBody').lastElementChild?.querySelector('input')?.focus()};
function tableAsk(title,text,fn){$('tableConfirmTitle').textContent=title;$('tableConfirmText').textContent=text;tablePending=fn;$('tableConfirmLayer').classList.remove('hidden')}
$('tableClearBtn').onclick=()=>tableAsk('清空全部记录','已录入的数据将被清空,并恢复为 3 条空白记录。',tableReset);
$('tableConfirmCancel').onclick=()=>{$('tableConfirmLayer').classList.add('hidden');tablePending=null};
$('tableConfirmOk').onclick=()=>{const fn=tablePending;$('tableConfirmLayer').classList.add('hidden');tablePending=null;fn?.()};
const tableHasData=()=>tableRows.some(r=>tableRequired.some(f=>f!=='status'&&String(r[f]||'').trim()));
function closeTable(){ $('tableModal').classList.add('hidden') }
function requestTableClose(){tableHasData()?tableAsk('放弃表格录入?','关闭后当前未提交内容不会保留。',()=>{closeTable();tableReset()}):closeTable()}
function setHybridView(view){hybridView=view;renderTableRows();const card=view==='card';$('hybridCards').classList.toggle('view-hidden',!card);$('hybridTable').classList.toggle('view-hidden',card);$('cardViewBtn').classList.toggle('active',card);$('tableViewBtn').classList.toggle('active',!card);$('viewTip').textContent=card?'卡片视图适合逐条录入和检查,切换到表格后数据保持不变。':'表格视图适合批量核对,可横向滚动查看全部字段。'}
$('cardViewBtn').onclick=()=>setHybridView('card');$('tableViewBtn').onclick=()=>setHybridView('table');
$('openTableBtn').onclick=()=>{tableReset();setHybridView('table');$('tableModal').classList.remove('hidden')};$('openHybridBtn').onclick=()=>{tableReset();setHybridView('card');$('tableModal').classList.remove('hidden')};$('tableCloseBtn').onclick=requestTableClose;$('tableCancelBtn').onclick=requestTableClose;
function validateTable(){tableErrors=[];tableRows.forEach(r=>{tableRequired.forEach(f=>{if(!String(r[f]??'').trim())tableErrors.push({key:r.key,field:f,msg:'此项为必填'})});if(r.quality_guaran_period&&Number(r.quality_guaran_period)<=0)tableErrors.push({key:r.key,field:'quality_guaran_period',msg:'必须大于 0'})});['package_box_sn','container_name'].forEach(f=>{const seen=new Map();tableRows.forEach((r,i)=>{const v=String(r[f]||'').trim();if(!v)return;if(seen.has(v)){const j=seen.get(v);tableErrors.push({key:r.key,field:f,msg:`与第 ${j+1} 行重复`});tableErrors.push({key:tableRows[j].key,field:f,msg:`与第 ${i+1} 行重复`})}else seen.set(v,i)})});renderTableRows();if(tableErrors.length){$('tableNotice').textContent=`发现 ${tableErrors.length} 处问题,请检查标红字段。`;$('tableNotice').classList.add('show');const first=tableErrors[0];document.querySelector(hybridView==='card'?`[data-hybrid-item="${first.key}-${first.field}"]`:`[data-table-cell="${first.key}-${first.field}"]`)?.scrollIntoView({block:'center',inline:'center'});return false}$('tableNotice').classList.remove('show');return true}
$('tableSubmitBtn').onclick=()=>{if(!validateTable())return;const b=$('tableSubmitBtn'),n=tableRows.length;b.disabled=true;b.textContent='正在提交…';setTimeout(()=>{const r=tableRows[0];$('listBody').insertAdjacentHTML('afterbegin',`<tr><td>${esc(r.package_box_sn)}</td><td>${esc(r.box_type.split(' ')[0])}</td><td>${r.box_length} × ${r.box_width} × ${r.box_high}</td><td>${esc(r.container_name)}</td><td>${esc(r.date_of_FG_inbound)}</td><td><span class="pill">${esc(r.status)}</span></td></tr>`);b.disabled=false;closeTable();toast(`表格批量新增成功,共 ${n}`);tableReset()},600)};
reset();tableReset();
</script>
</body>
</html>

File diff suppressed because one or more lines are too long