| CODE | 名称 | 分类 | 一品多码(CODE2) | 规格 | 零售价 | 操作 |
| 盘点单号 | 日期 | 门店 | 类型 | 状态 | 商品数 | 已盘数 | 盘盈 | 盘亏 | 操作人 | 操作 |
| CODE | 名称 | 分类 | 一品多码 | 规格 | 零售价 |
系统库存 | 实盘数量 | 差异 | 差异金额 | 状态 |
请上传库存 Excel:
需包含
商品编码 和
库存数量 两列;
可选
实盘数量(有值则自动录入并标为已盘);
可选
门店编码(筛选指定门店);
仅盘点库存 > 0 的商品。
下载示例 Excel 模板
let currentTab = 'product';
let currentCheckId = null;
let currentCheckData = null;
let scanQty = 1;
// ===================== 标签页切换 =====================
function switchTab(tab) {
currentTab = tab;
document.querySelectorAll('#checkTabs .nav-link').forEach(a => a.classList.remove('active'));
var tabLink = document.querySelector('[data-tab="' + tab + '"]');
if (tabLink) tabLink.classList.add('active');
document.querySelectorAll('.tab-content').forEach(function(d) { d.style.display = 'none'; });
var tabPanel = document.getElementById('tab-' + tab);
if (tabPanel) tabPanel.style.display = 'block';
if (tab === 'product') { loadProducts(); }
else if (tab === 'check-list') { loadChecks(); }
else if (tab === 'check-do') { loadCheckSelect(); }
else if (tab === 'store') { loadStores(); }
}
// ===================== 商品管理 =====================
async function loadProducts() {
let search = document.getElementById('productSearch').value;
let goodsType = document.getElementById('productTypeFilter').value;
let res = await fetch(`/check/api/products?search=${encodeURIComponent(search)}&goods_type=${encodeURIComponent(goodsType)}`);
let d = await res.json();
if (!d.success) return;
document.getElementById('productCount').textContent = d.data.length;
let html = d.data.map(p => `
| ${esc(p.code)} | ${esc(p.name)} | ${esc(p.goods_type||'')} |
${esc(p.code2||'')} | ${esc(p.spec||'')} |
¥${Number(p.rtlprc||0).toFixed(2)} |
|
`).join('');
document.getElementById('productTableBody').innerHTML = html || '| 暂无商品 |
';
// 更新分类下拉
let cats = [...new Set(d.data.map(p=>p.goods_type).filter(Boolean))].sort();
document.getElementById('productTypeFilter').innerHTML = '' + cats.map(c=>``).join('');
document.getElementById('catList').innerHTML = cats.map(c=>`';
}
} catch(e) { alert('清空失败:' + e.message); }
}
// ===================== 盘点管理 =====================
async function loadChecks() {
let store = document.getElementById('currentStore').value;
let url = '/check/api/checks';
if (store) url += '?store_code=' + encodeURIComponent(store);
let res = await fetch(url);
let d = await res.json();
if(!d.success) return;
let statusMap = {draft:'草稿', first_count:'初盘', recount:'复盘', finished:'已结束', confirmed:'已结束', cancelled:'已取消'};
let typeMap = {normal:'盘点', recount:'复盘'};
let html = d.data.map(c => `
| ${esc(c.check_no)} | ${c.check_date} |
${esc(c.store_name||c.store_code||'')} |
${typeMap[c.check_type]||typeMap['normal']} |
${statusMap[c.status]||c.status} |
${c.total_items} | ${c.checked_items||0} |
${c.profit_items||0} / ¥${Number(c.profit_amount||0).toFixed(2)} |
${c.loss_items||0} / ¥${Number(c.loss_amount||0).toFixed(2)} |
${esc(c.operator||'')} |
${['draft','first_count','recount'].includes(c.status)?``:''}
${(c.status==='finished'||c.status==='confirmed') && c.check_type!=='recount'?``:''}
|
`).join('');
document.getElementById('checkListBody').innerHTML = html || '| 暂无盘点记录 |
';
}
async function showCreateCheck() {
document.getElementById('newCheckOp').value = '管理员';
document.getElementById('newCheckFile').value = '';
document.getElementById('newCheckDate').value = new Date().toISOString().slice(0,10);
// 填充门店下拉
let res = await fetch('/check/api/stores');
let d = await res.json();
let sel = document.getElementById('newCheckStore');
sel.innerHTML = '' + d.data.map(s => ``).join('');
// 默认选中当前门店
let curStore = document.getElementById('currentStore').value;
if (curStore) sel.value = curStore;
new bootstrap.Modal('#createCheckModal').show();
}
async function submitCreateCheck() {
let op = document.getElementById('newCheckOp').value.trim();
let dt = document.getElementById('newCheckDate').value;
let store = document.getElementById('newCheckStore').value.trim();
let file = document.getElementById('newCheckFile').files[0];
if (!store) { alert('请选择盘点门店'); return; }
if (!file) { alert('请选择库存 Excel 文件'); return; }
let formData = new FormData();
formData.append('file', file);
formData.append('operator', op);
formData.append('check_date', dt);
formData.append('store_code', store);
let res = await fetch('/check/api/checks', {method:'POST', body: formData});
let d = await res.json();
alert(d.msg);
if (d.success) {
bootstrap.Modal.getInstance('#createCheckModal').hide();
loadChecks();
switchTab('check-do');
loadCheckSelect().then(()=>{
document.getElementById('checkSelect').value = d.check_id;
loadCheckDetail();
});
}
}
function viewCheck(cid) {
switchTab('check-do');
loadCheckSelect().then(()=>{
document.getElementById('checkSelect').value = cid;
loadCheckDetail();
});
}
async function delCheck(cid) {
if(!confirm('确定删除该盘点单?')) return;
let res = await fetch(`/check/api/checks/${cid}`, {method:'DELETE'});
let d = await res.json();
alert(d.msg);
if(d.success) loadChecks();
}
// ===================== 执行盘点 =====================
async function loadCheckSelect() {
let store = document.getElementById('currentStore').value;
let url = '/check/api/checks';
if (store) url += '?store_code=' + encodeURIComponent(store);
let res = await fetch(url);
let d = await res.json();
if(!d.success) return;
let sel = document.getElementById('checkSelect');
let typeLabel = (c) => c.check_type === 'recount' ? '复盘' : ({finished:'已结束', confirmed:'已结束', first_count:'初盘', draft:'草稿'}[c.status]||c.status);
sel.innerHTML = '' + d.data.map(c => ``).join('');
if(currentCheckId) sel.value = currentCheckId;
}
async function loadCheckDetail() {
let cid = document.getElementById('checkSelect').value;
if(!cid){ resetCheckView(); return; }
currentCheckId = parseInt(cid);
let res = await fetch(`/check/api/checks/${cid}`);
let d = await res.json();
if(!d.success){ alert(d.msg); return; }
currentCheckData = d.data;
let c = d.data;
let isFinished = c.status === 'finished' || c.status === 'confirmed';
// 状态
let statusText = {draft:'草稿', first_count:'初盘', recount:'复盘', finished:'已结束', confirmed:'已结束'}[c.status] || c.status;
let statusClass = {draft:'bg-secondary', first_count:'bg-warning', recount:'bg-info', finished:'bg-success', confirmed:'bg-success'}[c.status] || 'bg-secondary';
document.getElementById('checkStatusBadge').innerHTML = `${statusText}`;
// 统计
document.getElementById('statTotal').textContent = c.total_items;
document.getElementById('statChecked').textContent = c.checked_items || 0;
document.getElementById('statProfit').textContent = (c.profit_items||0) + ' / ¥' + Number(c.profit_amount||0).toFixed(2);
document.getElementById('statLoss').textContent = (c.loss_items||0) + ' / ¥' + Number(c.loss_amount||0).toFixed(2);
document.getElementById('checkStats').style.display = '';
// 扫码区
document.getElementById('scanArea').style.display = isFinished ? 'none' : '';
// 缓存全部商品明细
window._allCheckItems = c.items || [];
document.getElementById('filterBar').style.display = '';
document.getElementById('checkItemSearch').value = '';
// 根据是否复盘显示不同的表头和详情的汇总行
let isRecount = c && c.check_type === 'recount';
let isFinishedCheck = c && (c.status === 'finished' || c.status === 'confirmed');
let tableHead = document.querySelector('#checkItemsTable thead tr');
if (isRecount) {
tableHead.innerHTML = 'CODE | 名称 | 分类 | 一品多码 | 规格 | 零售价 | 系统库存 | 原盘实盘 | 复盘实盘 | 差异 | 差异金额 | 状态 | ';
} else {
tableHead.innerHTML = 'CODE | 名称 | 分类 | 一品多码 | 规格 | 零售价 | 系统库存 | 实盘数量 | 差异 | 差异金额 | 状态 | ';
}
// 显示复盘来源
if (isRecount && c.recount_of_no) {
document.getElementById('checkStatusBadge').innerHTML += ' (复盘自 ' + esc(c.recount_of_no) + ')';
}
document.getElementById('checkItemsTable').style.display = '';
// 确认后仍显示导出按钮,隐藏确认和删除
document.getElementById('checkActions').style.display = '';
document.getElementById('btnConfirmCheck').style.display = isFinished ? 'none' : '';
document.getElementById('btnDeleteCheck').style.display = isFinished ? 'none' : '';
document.getElementById('btnExportCheck').style.display = '';
document.getElementById('scanBarcode').focus();
// 更新刷新时间
updateRefreshTime();
// 启动自动轮询(若尚未启动)
startAutoRefresh();
}
let _autoRefreshTimer = null;
function startAutoRefresh() {
if (_autoRefreshTimer) return;
_autoRefreshTimer = setInterval(refreshCurrentCheck, 10000); // 每10秒自动刷新
}
function stopAutoRefresh() {
if (_autoRefreshTimer) { clearInterval(_autoRefreshTimer); _autoRefreshTimer = null; }
}
function updateRefreshTime() {
let now = new Date();
let ts = now.toLocaleTimeString('zh-CN', {hour12: false});
document.getElementById('refreshTime').textContent = '上次刷新 ' + ts;
document.getElementById('refreshTime').style.display = '';
}
async function refreshCurrentCheck() {
if (!currentCheckId) return;
try {
let res = await fetch(`/check/api/checks/${currentCheckId}`);
let d = await res.json();
if (!d.success) return;
// 对比数据是否变化
let changed = !currentCheckData ||
JSON.stringify(d.data.checked_items) !== JSON.stringify(currentCheckData.checked_items) ||
JSON.stringify(d.data.items.map(i => i.actual_qty)) !== JSON.stringify((currentCheckData.items||[]).map(i => i.actual_qty));
currentCheckData = d.data;
window._allCheckItems = d.data.items || [];
if (changed) {
renderCheckItems();
document.getElementById('statTotal').textContent = d.data.total_items;
document.getElementById('statChecked').textContent = d.data.checked_items || 0;
document.getElementById('statProfit').textContent = (d.data.profit_items||0) + ' / ¥' + Number(d.data.profit_amount||0).toFixed(2);
document.getElementById('statLoss').textContent = (d.data.loss_items||0) + ' / ¥' + Number(d.data.loss_amount||0).toFixed(2);
}
updateRefreshTime();
} catch(e) { /* 静默失败 */ }
}
function resetCheckView() {
currentCheckId = null; currentCheckData = null;
document.getElementById('checkStats').style.display = 'none';
document.getElementById('scanArea').style.display = 'none';
document.getElementById('checkItemsTable').style.display = 'none';
document.getElementById('checkActions').style.display = 'none';
document.getElementById('filterBar').style.display = 'none';
document.getElementById('checkStatusBadge').innerHTML = '';
document.getElementById('refreshTime').style.display = 'none';
window._allCheckItems = [];
stopAutoRefresh();
}
// ===================== 已盘 / 未盘分两区展示 =====================
function renderCheckItems() {
let search = (document.getElementById('checkItemSearch').value || '').trim().toLowerCase();
let items = window._allCheckItems || [];
let isFinished = currentCheckData && (currentCheckData.status === 'finished' || currentCheckData.status === 'confirmed');
// 按名称/编码搜索过滤
let applySearch = (arr) => {
if (!search) return arr;
return arr.filter(it =>
(it.name || '').toLowerCase().includes(search) ||
(it.code || '').toLowerCase().includes(search) ||
(it.code2 || '').toLowerCase().includes(search)
);
};
let unchecked = applySearch(items.filter(it => it.status !== 'checked'));
let checked = applySearch(items.filter(it => it.status === 'checked'));
let totalShowing = unchecked.length + checked.length;
document.getElementById('filterCount').textContent =
search ? `搜索 "${search}":${totalShowing} 条` : '';
document.getElementById('filterUnchecked').textContent = unchecked.length;
document.getElementById('filterChecked').textContent = checked.length;
// 行渲染函数
let isRecount = currentCheckData && currentCheckData.check_type === 'recount';
let colSpan = isRecount ? 13 : 11;
let rowHtml = (it) => {
let diffQty = Number(it.diff_qty||0);
let diffCls = diffQty > 0 ? 'diff-positive' : (diffQty < 0 ? 'diff-negative' : 'diff-zero');
let rowCls = it.status === 'checked' ? 'item-checked' : 'item-pending';
let originalActual = it.original_actual_qty != null ? Number(it.original_actual_qty) : '-';
// 复盘模式显示原盘实盘列
let actualCell;
if (isFinished || it.status==='checked') {
actualCell = `${Number(it.actual_qty||0)}`;
} else {
actualCell = ``;
}
if (isRecount) {
return `
| ${esc(it.code)} | ${esc(it.name)} | ${esc(it.goods_type||'')} |
${esc(it.code2||'')} | ${esc(it.spec||'')} |
¥${Number(it.rtlprc||0).toFixed(2)} |
${Number(it.system_qty||0)} |
${originalActual} |
${actualCell} |
${diffQty>0?'+':''}${diffQty} |
${Number(it.diff_amount||0)!==0?(Number(it.diff_amount||0)>0?'+':'')+Number(it.diff_amount||0).toFixed(2):'-'} |
${it.status==='checked'?'已盘':'待盘'} |
`;
} else {
return `
| ${esc(it.code)} | ${esc(it.name)} | ${esc(it.goods_type||'')} |
${esc(it.code2||'')} | ${esc(it.spec||'')} |
¥${Number(it.rtlprc||0).toFixed(2)} |
${Number(it.system_qty||0)} |
${actualCell} |
${diffQty>0?'+':''}${diffQty} |
${Number(it.diff_amount||0)!==0?(Number(it.diff_amount||0)>0?'+':'')+Number(it.diff_amount||0).toFixed(2):'-'} |
${it.status==='checked'?'已盘':'待盘'} |
`;
}
};
let sections = [];
// 未盘区域
if (unchecked.length > 0) {
sections.push(`|
⚠ 未盘商品 (${unchecked.length})
|
`);
sections.push(unchecked.map(rowHtml).join(''));
}
// 已盘区域
if (checked.length > 0) {
sections.push(`|
✓ 已盘商品 (${checked.length})
|
`);
sections.push(checked.map(rowHtml).join(''));
}
if (sections.length === 0) {
document.getElementById('checkItemsBody').innerHTML =
`| ${search ? '无匹配商品' : '暂无商品数据'} |
`;
} else {
document.getElementById('checkItemsBody').innerHTML = sections.join('');
}
}
async function updateItem(itemId, val) {
let qty = parseInt(val);
if(isNaN(qty)) return;
let res = await fetch(`/check/api/checks/${currentCheckId}/items`, {
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({item_id:itemId, actual_qty:qty})
});
let d = await res.json();
if (d.success) {
// 部分刷新:重新拉取数据并渲染,保持筛选状态
let r2 = await fetch(`/check/api/checks/${currentCheckId}`);
let d2 = await r2.json();
if (d2.success) {
currentCheckData = d2.data;
window._allCheckItems = d2.data.items || [];
renderCheckItems();
document.getElementById('statTotal').textContent = d2.data.total_items;
document.getElementById('statChecked').textContent = d2.data.checked_items || 0;
document.getElementById('statProfit').textContent = (d2.data.profit_items||0) + ' / ¥' + Number(d2.data.profit_amount||0).toFixed(2);
document.getElementById('statLoss').textContent = (d2.data.loss_items||0) + ' / ¥' + Number(d2.data.loss_amount||0).toFixed(2);
}
} else alert(d.msg);
}
function setScanQty(q) { scanQty = q; document.getElementById('scanQty').value = q; }
// 扫码回车事件
let scanLookupTimer = null;
document.addEventListener('DOMContentLoaded', ()=>{
// 初始化门店选择器
loadStores();
// 加载全部商品资料供扫码搜索
loadAllProductsForScan();
// 默认第一个 tab
switchTab('check-do');
// 扫码输入框:实时模糊搜索更新 datalist
document.getElementById('scanBarcode').addEventListener('input', function(){
clearTimeout(scanLookupTimer);
let val = this.value.trim();
if (val.length < 2) {
document.getElementById('scanDatalist').innerHTML = '';
document.getElementById('scanMatchList').style.display = 'none';
return;
}
scanLookupTimer = setTimeout(() => updateScanDatalist(val), 250);
});
});
// 加载全部商品资料供扫码搜索
async function loadAllProductsForScan() {
let res = await fetch('/check/api/products?search=&goods_type=');
let d = await res.json();
if (d.success) window._allProducts = d.data;
}
// 匹配函数:CODE 精确 | 名称 包含 | code2 后缀
function matchesKeyword(item, kw) {
let code = (item.code || '').toLowerCase();
let name = (item.name || '').toLowerCase();
let code2 = (item.code2 || '').toLowerCase();
if (code === kw) return true;
if (name.includes(kw)) return true;
let lines = code2.split('\n').map(l => l.trim()).filter(Boolean);
if (lines.some(line => line.endsWith(kw))) return true;
return false;
}
// 更新扫码输入框的 datalist(搜索盘点单商品 + 全部商品资料)
async function updateScanDatalist(keyword) {
if (!currentCheckId) return;
let kw = keyword.toLowerCase();
let checkItems = window._allCheckItems || [];
let allProducts = window._allProducts || [];
// 盘点单内商品匹配
let checkMatches = checkItems.filter(it => matchesKeyword(it, kw));
// 全部商品资料匹配(排除已在盘点单中的)
let checkProductIds = new Set(checkItems.map(it => it.product_id));
let productMatches = allProducts.filter(p =>
!checkProductIds.has(p.id) && matchesKeyword(p, kw)
);
// 盘点单商品优先显示
let matches = [...checkMatches, ...productMatches].slice(0, 15);
let list = document.getElementById('scanDatalist');
list.innerHTML = matches.map(it =>
``
).join('');
}
// 扫码回车处理
async function scanBarcodeEnter() {
let input = document.getElementById('scanBarcode');
let barcode = input.value.trim();
if (!barcode || !currentCheckId) return;
let resultDiv = document.getElementById('scanResult');
document.getElementById('scanMatchList').style.display = 'none';
// 如果已经通过模糊匹配选中了商品,直接提交
if (window._selectedScanItem) {
let item = window._selectedScanItem;
let qty = parseInt(document.getElementById('scanQty').value) || 1;
await doQuickScan(item, qty);
return;
}
// 尝试解析 "编码 - 名称" 格式
let actualCode = barcode.split(' - ')[0].trim();
let res = await fetch(`/check/api/checks/${currentCheckId}/barcode-lookup`, {
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({barcode: actualCode})
});
let d = await res.json();
if (d.success) {
if (d.multiple) {
// 多个匹配结果,展示选择列表
showScanMatchList(d.matches);
return;
}
// 单个匹配,直接提交数量
await doQuickScan(d.item, parseInt(document.getElementById('scanQty').value) || 1);
} else {
resultDiv.innerHTML = `${d.msg}
`;
}
}
// 展示模糊匹配的商品选择列表
function showScanMatchList(matches) {
let container = document.getElementById('scanMatchItems');
container.innerHTML = matches.map(m => {
let display = `${esc(m.code)} - ${esc(m.name)}`;
return ``;
}).join('');
document.getElementById('scanMatchList').style.display = '';
document.getElementById('scanResult').innerHTML = '';
}
// 选择模糊匹配的商品
async function selectScanMatch(code, name) {
document.getElementById('scanMatchList').style.display = 'none';
document.getElementById('scanBarcode').value = code + ' - ' + name;
document.getElementById('scanResult').innerHTML = `已选择: ${esc(code)} - ${esc(name)},请调整数量后按回车确认
`;
document.getElementById('scanQty').focus();
document.getElementById('scanQty').select();
// 保存当前选中的商品信息,按回车时直接提交
window._selectedScanItem = { code: code, name: name };
}
// 快速扫码提交
async function doQuickScan(item, qty) {
let res = await fetch(`/check/api/checks/${currentCheckId}/barcode-scan`, {
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({barcode: item.code, qty: qty})
});
let d = await res.json();
let resultDiv = document.getElementById('scanResult');
if(d.success){
resultDiv.innerHTML = `${d.msg} (已盘 ${d.checked_items}/${d.total_items})
`;
let r2 = await fetch(`/check/api/checks/${currentCheckId}`);
let d2 = await r2.json();
if (d2.success) {
currentCheckData = d2.data;
window._allCheckItems = d2.data.items || [];
renderCheckItems();
document.getElementById('statTotal').textContent = d2.data.total_items;
document.getElementById('statChecked').textContent = d2.data.checked_items || 0;
document.getElementById('statProfit').textContent = (d2.data.profit_items||0) + ' / ¥' + Number(d2.data.profit_amount||0).toFixed(2);
document.getElementById('statLoss').textContent = (d2.data.loss_items||0) + ' / ¥' + Number(d2.data.loss_amount||0).toFixed(2);
}
} else {
resultDiv.innerHTML = `${d.msg}
`;
}
document.getElementById('scanBarcode').value = '';
document.getElementById('scanDatalist').innerHTML = '';
delete window._selectedScanItem;
document.getElementById('scanBarcode').focus();
}
async function confirmCheck() {
if(!currentCheckId) return;
if(!confirm('确认盘点?确认后不可修改。')) return;
let res = await fetch(`/check/api/checks/${currentCheckId}/confirm`, {method:'POST'});
let d = await res.json();
alert(d.msg);
if(d.success) loadCheckDetail();
}
function exportCheck() {
if(!currentCheckId) return;
window.open(`/check/api/checks/${currentCheckId}/export`, '_blank');
}
async function deleteCheck() {
if(!currentCheckId) return;
if(!confirm('确定要删除该盘点单吗?此操作不可恢复!')) return;
let res = await fetch(`/check/api/checks/${currentCheckId}`, {method:'DELETE'});
let d = await res.json();
alert(d.msg);
if(d.success) {
currentCheckId = null;
currentCheckData = null;
location.reload();
}
}
async function doRecount(cid) {
if(!confirm('确定对该盘点单进行复盘吗?将创建一份复盘单,重新盘点全部商品。')) return;
let operator = prompt('操作人:', '管理员') || '管理员';
let res = await fetch(`/check/api/checks/${cid}/recount`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({operator: operator})
});
let d = await res.json();
alert(d.msg);
if(d.success) {
loadChecks();
switchTab('check-do');
loadCheckSelect().then(()=>{
document.getElementById('checkSelect').value = d.check_id;
loadCheckDetail();
});
}
}
// ===================== 门店管理 =====================
let _allStoreData = [];
async function loadStores() {
let res = await fetch('/check/api/stores');
let d = await res.json();
if(!d.success) return;
_allStoreData = d.data;
// 更新门店搜索 datalist
updateStoreDatalist(d.data);
// 更新门店管理表格
let html = d.data.map(s => `
| ${esc(s.code)} | ${esc(s.name)} |
${esc(s.region||'')} | ${esc(s.area||'')} | ${esc(s.store_type||'')} |
|
`).join('');
document.getElementById('storeTableBody').innerHTML = html || '| 暂无门店 |
';
document.getElementById('storeCount').textContent = d.data.length;
}
function updateStoreDatalist(stores) {
let list = document.getElementById('storeDatalist');
list.innerHTML = stores.map(s => ``).join('');
}
// 根据输入内容匹配门店编码(输入可能是编码、名称或完整结果)
function resolveStoreCode(inputVal) {
let v = inputVal.trim();
if (!v || !_allStoreData.length) return '';
// 优先从完整结果中提取编码(如:073180 - 购玖(汇金天虹店))
let codePart = v.split(' - ')[0].trim();
let exact = _allStoreData.find(s => s.code === codePart);
if (exact) return exact.code;
// 精确匹配编码
exact = _allStoreData.find(s => s.code === v);
if (exact) return exact.code;
// 模糊匹配编码开头
let prefix = _allStoreData.find(s => s.code.startsWith(v));
if (prefix) return prefix.code;
// 匹配名称包含
let nameMatch = _allStoreData.find(s => s.name.includes(v));
if (nameMatch) return nameMatch.code;
return '';
}
async function confirmStore() {
let input = document.getElementById('currentStore');
let storeCode = resolveStoreCode(input.value);
let badge = document.getElementById('currentStoreBadge');
if (!storeCode) {
alert('未匹配到门店,请重新输入编码或名称');
return;
}
let store = _allStoreData.find(s => s.code === storeCode);
let display = storeCode + ' - ' + (store ? store.name : '');
badge.textContent = display;
badge.style.display = '';
input.value = display;
// 保存到 session
await fetch('/check/api/stores/current', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({store_code: storeCode, store_name: store ? store.name : ''})
});
// 刷新相关列表
if (currentTab === 'check-list') loadChecks();
else if (currentTab === 'check-do') loadCheckSelect();
}
function showStoreForm(sid) {
document.getElementById('storeId').value = sid || '';
document.getElementById('storeModalTitle').textContent = sid ? '编辑门店' : '添加门店';
if (sid) {
fetch('/check/api/stores').then(r=>r.json()).then(d=>{
let s = d.data.find(x=>x.id==sid);
if(s){
document.getElementById('sCode').value = s.code||'';
document.getElementById('sName').value = s.name||'';
document.getElementById('sRegion').value = s.region||'';
document.getElementById('sArea').value = s.area||'';
document.getElementById('sType').value = s.store_type||'';
}
});
} else {
document.getElementById('sCode').value = '';
document.getElementById('sName').value = '';
document.getElementById('sRegion').value = '';
document.getElementById('sArea').value = '';
document.getElementById('sType').value = '';
}
new bootstrap.Modal('#storeModal').show();
}
async function saveStore() {
let sid = document.getElementById('storeId').value;
let body = {
id: sid ? parseInt(sid) : null,
code: document.getElementById('sCode').value.trim(),
name: document.getElementById('sName').value.trim(),
region: document.getElementById('sRegion').value.trim(),
area: document.getElementById('sArea').value.trim(),
store_type: document.getElementById('sType').value.trim()
};
let res = await fetch('/check/api/stores', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
let d = await res.json();
alert(d.msg);
if(d.success){ bootstrap.Modal.getInstance('#storeModal').hide(); loadStores(); }
}
function editStore(sid) { showStoreForm(sid); }
async function delStore(sid) {
if(!confirm('确定删除该门店吗?')) return;
let res = await fetch('/check/api/stores/' + sid, {method:'DELETE'});
let d = await res.json();
alert(d.msg);
if(d.success) loadStores();
}
async function importStores(input) {
let file = input.files[0];
if (!file) return;
let formData = new FormData();
formData.append('file', file);
let res = await fetch('/check/api/stores/import', {method:'POST', body:formData});
let d = await res.json();
alert(d.msg);
input.value = '';
if (d.success) loadStores();
}
function downloadStoreTemplate() {
window.open('/check/api/stores/template', '_blank');
}
let _html5QrCode = null;
let _currentCameraId = null;
let _cameraList = [];
function openCameraScan() {
if (!currentCheckId) { alert('请先选择盘点单'); return; }
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
alert('当前浏览器不支持摄像头扫码,请使用 Chrome/Edge/Safari 等现代浏览器,并确保站点为 HTTPS');
return;
}
document.getElementById('cameraScanModal').style.display = '';
startCameraScan();
}
function closeCameraScan() {
document.getElementById('cameraScanModal').style.display = 'none';
stopCameraScan();
}
async function startCameraScan() {
let region = document.getElementById('cameraScanRegion');
region.innerHTML = '';
try {
if (!_html5QrCode) {
_html5QrCode = new Html5Qrcode('cameraScanRegion');
}
// 获取可用摄像头
let devices = await Html5Qrcode.getCameras();
_cameraList = devices || [];
if (!_cameraList.length) {
region.innerHTML = '未检测到摄像头,请检查设备权限。
';
return;
}
if (!_currentCameraId) _currentCameraId = _cameraList[0].id;
// 后置摄像头优先(移动端)
let back = _cameraList.find(c => /back|rear|environment/i.test(c.label));
if (back) _currentCameraId = back.id;
await _html5QrCode.start(
_currentCameraId,
{ fps: 10, qrbox: { width: 250, height: 150 }, aspectRatio: 1.777 },
onCameraScanSuccess,
() => {}
);
} catch(e) {
region.innerHTML = '摄像头启动失败:' + esc(e.message || '请检查摄像头权限') + '
';
}
}
function stopCameraScan() {
if (_html5QrCode) {
try { _html5QrCode.stop().catch(()=>{}); } catch(e) {}
}
}
async function switchCamera() {
if (_cameraList.length < 2) return;
let idx = _cameraList.findIndex(c => c.id === _currentCameraId);
_currentCameraId = _cameraList[(idx + 1) % _cameraList.length].id;
stopCameraScan();
await startCameraScan();
}
async function onCameraScanSuccess(decodedText) {
// 扫描成功:填入条码,关闭弹窗,执行扫码录入
document.getElementById('scanBarcode').value = decodedText;
closeCameraScan();
await scanBarcodeEnter();
}
function esc(s) {
if(!s) return '';
let d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}