feat: 入库/出库单笔录入新增物料编码输入框(输入或扫码自动定位)

- 单笔录入表单顶部新增「物料编码」输入框, 输入或扫码物料编码即自动定位下拉物料:
  精确命中/前缀唯一立即锁定并把光标跳到数量; 多个前缀候选提示继续输入; 编码不存在标红提示
- 下拉保留作后备, 手选下拉会反向自动填入编码, 两种方式互通
- option 加 data-pn 携带物料编码供前端精确匹配, 不改后端提交逻辑(仍用 material_id)
- 新增 .pn-lookup / .pn-hint 样式(状态色与系统 ok/danger/warn 一致)
- 教程补充单笔录入物料编码输入框说明

(注: 此前模拟齐套 commit 9f29139 因网络断连未推送, 本次一并推送)
This commit is contained in:
2026-09-23 11:14:27 +08:00
parent 9f291398a1
commit 439877c6ba
3 changed files with 62 additions and 3 deletions
+50 -2
View File
@@ -11,11 +11,16 @@
</div>
<form method="post" class="card form-card txn-form">
<label>物料编码(输入或扫码自动定位)</label>
<div class="pn-lookup">
<input id="pnLookup" placeholder="输入物料编码,如 P0001,回车自动定位" autocomplete="off">
<span id="pnHint" class="pn-hint"></span>
</div>
<label>物料 *</label>
<select name="material_id" required>
<select name="material_id" id="materialSelect" required>
<option value="">-- 选择物料 --</option>
{% for m in mats %}
<option value="{{ m.id }}">{{ m.part_number }} ({{ m.manufacturer or '-' }})</option>
<option value="{{ m.id }}" data-pn="{{ m.part_number }}">{{ m.part_number }} ({{ m.manufacturer or '-' }})</option>
{% endfor %}
</select>
<label>批次号</label>
@@ -96,6 +101,49 @@ P0002 50 2026-09-11 到货B"></textarea>
const TXN_KIND = "{{ kind }}";
let lastRows = [];
/* ---- 物料编码输入框: 输入/扫码自动定位下拉物料 ---- */
(function(){
const sel = document.getElementById('materialSelect');
const pn = document.getElementById('pnLookup');
const hint = document.getElementById('pnHint');
if(!sel || !pn || !hint) return;
function lockTo(o){
sel.value = o.value;
hint.textContent = '✓ ' + o.text;
hint.className = 'pn-hint ok';
const qty = document.querySelector('.txn-form input[name=qty]');
if(qty) qty.focus();
}
function locate(){
const q = pn.value.trim().toUpperCase();
if(!q){ hint.textContent=''; hint.className='pn-hint'; return; }
const opts = Array.from(sel.options).filter(o => o.value !== '');
const exact = opts.find(o => (o.getAttribute('data-pn')||'').toUpperCase() === q);
if(exact){ lockTo(exact); return; }
const prefix = opts.filter(o => (o.getAttribute('data-pn')||'').toUpperCase().startsWith(q));
if(prefix.length === 1){ lockTo(prefix[0]); return; }
if(prefix.length > 1){
hint.textContent = '… 多个候选(' + prefix.length + '),继续输入';
hint.className = 'pn-hint warn';
return;
}
hint.textContent = '✗ 未找到物料编码:' + q;
hint.className = 'pn-hint danger';
}
pn.addEventListener('input', locate);
pn.addEventListener('keydown', e => { if(e.key === 'Enter'){ e.preventDefault(); locate(); } });
sel.addEventListener('change', () => {
const o = sel.options[sel.selectedIndex];
if(o && o.value !== ''){
pn.value = o.getAttribute('data-pn') || '';
hint.textContent = '✓ ' + o.text;
hint.className = 'pn-hint ok';
} else {
pn.value = ''; hint.textContent = ''; hint.className = 'pn-hint';
}
});
})();
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(); });