129 lines
7.9 KiB
HTML
129 lines
7.9 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}BOM 清单{% endblock %}
|
||
{% block content %}
|
||
<div class="toolbar" data-page-node-id="vZTw3kPadHZliSPinOK7Z1">
|
||
<h3 style="margin:0" data-page-node-id="4wsrdtS5FO7LVSlfo7CSNG">BOM 清单 ({{ data|length }})</h3>
|
||
<div data-page-node-id="wqIH8X3KYDPieHFmuHJk1U">
|
||
<a class="btn" href="{{ url_for('download_template', kind='bom') }}" data-page-node-id="1uWZtIhFdzaXWtcVfxDuOg">⬇ 下载模板</a>
|
||
<button class="primary" type="button" onclick="openBatch()" data-page-node-id="gzZTAfVl4A7uSSGZbnvUiB">⊞ 批量导入窗口</button>
|
||
</div>
|
||
</div>
|
||
<table class="tbl" data-page-node-id="b0iXNI8PpWQuniQYav5O0I">
|
||
<thead data-page-node-id="iYtImpRt9uTnLT1EuDXgCb"><tr data-page-node-id="9EW4NPakt3ZhDfzYevNQb6"><th data-page-node-id="sp7aqyu2wPcBXC14JySMsV">BOM 型号</th><th data-page-node-id="ON7POYFoxBWVPwDTJ5YFVh">名称</th><th data-page-node-id="H6pAPeWhIGXasJf7is1PlR">组成项数</th><th data-page-node-id="IQPsDz71IQrUtVnzjSvTqE">操作</th></tr></thead>
|
||
<tbody data-page-node-id="GlcDs3bYGt9ktLuIMCnQ4S">
|
||
{% for d in data %}
|
||
<tr data-page-node-id="oIa73ogUxPXPW7hd6mSQqR">
|
||
<td data-page-node-id="txkFINu0hqDcunfToMsIHJ"><a href="{{ url_for('bom_detail', bid=d.b.id) }}" data-page-node-id="wZa7pJWwB3UVob7w2rMPYi">{{ d.b.code }}</a></td>
|
||
<td data-page-node-id="4ZgT9JXaM292L2DxjmPWdn">{{ d.b.name or '-' }}</td>
|
||
<td class="num" data-page-node-id="81E2RDtrSvIDDtmXamJaKy"><a href="{{ url_for('bom_detail', bid=d.b.id) }}" data-page-node-id="xibV2oxiCVTyWQxHQ3GBvC">{{ d.count }}</a></td>
|
||
<td data-page-node-id="z8hNVcUqGGY3LogEDYFO6M">
|
||
<a href="{{ url_for('bom_detail', bid=d.b.id) }}" data-page-node-id="y99zemTyOQfPCzgAuGfoHX">查看</a>
|
||
<a href="{{ url_for('bom_edit', bid=d.b.id) }}" data-page-node-id="Xlkt2Bi0z38orXYfmovRTF">编辑</a>
|
||
<form method="post" action="{{ url_for('bom_delete', bid=d.b.id) }}" style="display:inline" onsubmit="return confirm('确认删除该 BOM?');" data-page-node-id="GRCffEreUmFcM3CoH1FEUC">
|
||
<button class="link" data-page-node-id="fk4q7Q2tIJdpd368XDifLE">删除</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr data-page-node-id="8dATczjWBQGQA7MAsGu9f8"><td colspan="4" class="muted" data-page-node-id="7Zbca69N7opSKetW07LkHr">还没有 BOM, 点右上角新增</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
|
||
<!-- 批量导入窗口(模态) -->
|
||
<div class="modal-mask" id="batchMask" style="display:none" data-page-node-id="na2dwI5Srtijy3wkeG1h5s">
|
||
<div class="modal" data-page-node-id="tEI8MSJjVrQXENBtLlS1dA">
|
||
<div class="modal-head" data-page-node-id="EHG4VZ8LF2jK6OQuZpZ1ij">
|
||
<span data-page-node-id="xHsrz06nBQLPcxe4wbnUF5">批量导入 BOM · 多 BOM 清单</span>
|
||
<button class="x" onclick="closeBatch()" data-page-node-id="aoDJmcOoqnCSk7ji1ol2al">✕</button>
|
||
</div>
|
||
<div class="modal-body" data-page-node-id="v1ZVsZhJV9kTmwR8H3UTll">
|
||
<p class="small muted" data-page-node-id="uBFCxuwHOoYf4HDB7GHjqB">支持两种录入:① 直接上传 <b data-page-node-id="qPxNhhrFxrTrmWD4tIcNEf">Altium / 嘉立创 BOM</b>(每个 worksheet 即一个 BOM,<b data-page-node-id="6TXeirBIr5BJkAkpI9CMod">Part Number</b> 关联物料、<b data-page-node-id="NYIE9aPWd3xD4Tr0hLOqI3">Quantity</b> 列即单套用量,下载模板即此格式);② 粘贴文本,每行一个 BOM,以 <b data-page-node-id="jy8nTX15GDqKSV6wYSm78A">#BOM <型号> <名称></b> 开头,其下为 <b data-page-node-id="Lgr6U8MBrInBJO29ZLmnN3">物料编号 单套用量</b>,空行分隔。物料不存在时导入会自动新建主数据。</p>
|
||
<textarea id="batchText" rows="11" placeholder="#BOM M4021 示例产品A
|
||
P0001 2
|
||
P0002 0.5
|
||
|
||
#BOM M4022 示例产品B
|
||
P0003 4"></textarea>
|
||
<div class="row-actions" data-page-node-id="Un0xhQE1iuii1GXRoLOGEy">
|
||
<button type="button" onclick="parsePaste()" data-page-node-id="2nie414a2o4ULz4FNMbFVC">解析预览</button>
|
||
<label class="btn" style="cursor:pointer" data-page-node-id="s3iZeq1kL8Etsahvv2hGKO">📁 上传文件
|
||
<input type="file" id="batchFile" accept=".xlsx,.csv" style="display:none" onchange="parseFile()" data-page-node-id="sj1omvbTTh7dteUj3QePnl">
|
||
</label>
|
||
<button type="button" class="primary" id="commitBtn" onclick="commitBatch()" disabled data-page-node-id="LV1nhOjsxrIhBszlCoxA2s">确认导入</button>
|
||
</div>
|
||
<div id="batchResult" style="margin-top:12px" data-page-node-id="zdtCCsWu38gEMMgkruNxOI"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let lastBoms = [];
|
||
|
||
function openBatch(){ document.getElementById('batchMask').style.display='flex'; }
|
||
function closeBatch(){ document.getElementById('batchMask').style.display='none'; }
|
||
document.getElementById('batchMask').addEventListener('click', e=>{ if(e.target.id==='batchMask') closeBatch(); });
|
||
|
||
function parsePaste(){
|
||
const text = document.getElementById('batchText').value.trim();
|
||
if(!text){ alert('请先粘贴清单'); return; }
|
||
postPreview({text: text});
|
||
}
|
||
function parseFile(){
|
||
const f = document.getElementById('batchFile').files[0];
|
||
if(!f) return;
|
||
const fd = new FormData();
|
||
fd.append('file', f);
|
||
fetch('/api/import/boms/preview', {method:'POST', body:fd})
|
||
.then(r=>r.json()).then(renderPreview).catch(e=>alert('解析失败: '+e));
|
||
}
|
||
function postPreview(payload){
|
||
fetch('/api/import/boms/preview', {
|
||
method:'POST', headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify(payload)
|
||
}).then(r=>r.json()).then(renderPreview).catch(e=>alert('解析失败: '+e));
|
||
}
|
||
function esc(s){
|
||
return String(s==null?'':s).replace(/[&<>"]/g, c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
||
}
|
||
function renderPreview(data){
|
||
lastBoms = data.boms || [];
|
||
const valid = data.valid, invalid = data.invalid;
|
||
let html = `<div class="flash ${invalid? 'error':'ok'}">共 ${lastBoms.length} 个 BOM · 可导入 <b>${valid}</b> 个 · 异常 <b>${invalid}</b> 个</div>`;
|
||
lastBoms.forEach((b, i)=>{
|
||
const btag = b.ok? '<span class="badge ok">OK</span>' : `<span class="badge danger">${b.error||'错误'}</span>`;
|
||
html += `<div class="bom-card ${b.ok?'':'card-danger'}">`;
|
||
html += `<div class="bom-head">`;
|
||
html += `<label class="bh-lbl">型号</label><input class="bom-code-inp" value="${esc(b.code)}" oninput="lastBoms[${i}].code=this.value" title="可编辑 BOM 型号">`;
|
||
html += `<label class="bh-lbl">名称</label><input class="bom-name-inp" value="${esc(b.name)}" oninput="lastBoms[${i}].name=this.value" title="可编辑 BOM 名称(全部显示,可修改)">`;
|
||
html += `${btag} <span class="muted small">成功项 ${b.valid_items}/${b.items.length}</span></div>`;
|
||
html += '<table class="tbl"><thead><tr><th>物料编号</th><th>型号/规格</th><th>单套用量</th><th>校验</th></tr></thead><tbody>';
|
||
b.items.forEach(it=>{
|
||
const cls = it.ok? '' : 'row-danger';
|
||
let tag;
|
||
if (it.auto_create) tag = '<span class="badge warn">新建</span>';
|
||
else if (it.ok) tag = '<span class="badge ok">OK</span>';
|
||
else tag = `<span class="badge danger">${it.error||'错误'}</span>`;
|
||
html += `<tr class="${cls}"><td>${it.part_number||''}</td><td class="muted small">${it.manufacturer||''}</td><td class="num">${it.qty_per_unit}</td><td>${tag}</td></tr>`;
|
||
});
|
||
html += '</tbody></table></div>';
|
||
});
|
||
document.getElementById('batchResult').innerHTML = html;
|
||
document.getElementById('commitBtn').disabled = valid===0;
|
||
}
|
||
function commitBatch(){
|
||
if(!lastBoms.length) return;
|
||
fetch('/api/import/boms/commit', {
|
||
method:'POST', headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify({boms: lastBoms})
|
||
}).then(r=>r.json()).then(d=>{
|
||
let msg = `导入完成: ${d.boms} 个 BOM, 成功 ${d.added_items} 项`;
|
||
if (d.created_materials) msg += `, 新建物料 ${d.created_materials}`;
|
||
if (d.skipped_items) msg += `, 跳过 ${d.skipped_items} 项`;
|
||
alert(msg);
|
||
if(d.boms>0) location.reload();
|
||
}).catch(e=>alert('导入失败: '+e));
|
||
}
|
||
</script>
|
||
{% endblock %}
|