370 lines
14 KiB
JavaScript
370 lines
14 KiB
JavaScript
// BOM物料清单管理
|
||
(() => {
|
||
let bomList = [];
|
||
let currentPage = 1;
|
||
const pageSize = 20;
|
||
let editingId = null;
|
||
|
||
Router.register('/plan-mgmt/bom', async () => {
|
||
const html = `
|
||
<div class="page-header">
|
||
<h1>BOM物料清单</h1>
|
||
<div class="page-actions">
|
||
<button id="add-bom-btn" class="btn btn-primary">新增BOM</button>
|
||
<button id="import-bom-btn" class="btn btn-secondary">📥 导入Excel</button>
|
||
<button id="download-template-btn" class="btn btn-secondary">📄 下载模板</button>
|
||
<button id="batch-delete-btn" class="btn btn-danger">批量删除</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-body">
|
||
<div class="filter-section" style="margin-bottom: 16px;">
|
||
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
||
<input type="text" id="search-keyword" class="input" placeholder="搜索产品编码/名称/物料..." style="width: 300px;" />
|
||
<button class="btn btn-primary" onclick="BOM.search()">搜索</button>
|
||
<button class="btn btn-secondary" onclick="BOM.resetSearch()">重置</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-container" style="overflow-x: auto;">
|
||
<table class="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 40px;"><label class="custom-checkbox"><input type="checkbox" id="select-all" onchange="BOM.toggleSelectAll(this)" /><span class="checkmark"></span></label></th>
|
||
<th>产品编码</th>
|
||
<th>产品名称</th>
|
||
<th>物料编码</th>
|
||
<th>物料名称</th>
|
||
<th>单机用量</th>
|
||
<th>单位</th>
|
||
<th>最小包装</th>
|
||
<th>供应商</th>
|
||
<th>备注</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="bom-list">
|
||
<tr><td colspan="11" class="text-center">加载中...</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="pagination" id="pagination" style="margin-top: 16px; display: flex; justify-content: center; gap: 8px;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 新增/编辑BOM弹窗 -->
|
||
<div id="bom-modal" class="modal" style="display:none;">
|
||
<div class="modal-content" style="max-width: 600px;">
|
||
<div class="modal-header">
|
||
<h2 id="modal-title">新增BOM</h2>
|
||
<button class="modal-close" onclick="BOM.closeModal()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="bom-form">
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
|
||
<div class="field">
|
||
<label>产品编码 <span style="color: var(--danger);">*</span></label>
|
||
<input type="text" id="product-code" class="input" required />
|
||
</div>
|
||
<div class="field">
|
||
<label>产品名称 <span style="color: var(--danger);">*</span></label>
|
||
<input type="text" id="product-name" class="input" required />
|
||
</div>
|
||
<div class="field">
|
||
<label>物料编码 <span style="color: var(--danger);">*</span></label>
|
||
<input type="text" id="material-code" class="input" required />
|
||
</div>
|
||
<div class="field">
|
||
<label>物料名称 <span style="color: var(--danger);">*</span></label>
|
||
<input type="text" id="material-name" class="input" required />
|
||
</div>
|
||
<div class="field">
|
||
<label>单机用量 <span style="color: var(--danger);">*</span></label>
|
||
<input type="number" id="unit-qty" class="input" step="0.01" min="0.01" value="1" required />
|
||
</div>
|
||
<div class="field">
|
||
<label>单位</label>
|
||
<input type="text" id="unit" class="input" value="pcs" />
|
||
</div>
|
||
<div class="field">
|
||
<label>最小包装</label>
|
||
<input type="number" id="min-package" class="input" min="1" value="1" />
|
||
</div>
|
||
<div class="field">
|
||
<label>供应商</label>
|
||
<input type="text" id="supplier" class="input" />
|
||
</div>
|
||
</div>
|
||
<div class="field" style="margin-top: 16px;">
|
||
<label>备注</label>
|
||
<textarea id="remark" class="input" rows="2"></textarea>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" onclick="BOM.closeModal()">取消</button>
|
||
<button class="btn btn-primary" onclick="BOM.save()">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
setTimeout(() => {
|
||
document.getElementById('add-bom-btn')?.addEventListener('click', () => openModal());
|
||
document.getElementById('import-bom-btn')?.addEventListener('click', () => showImportDialog());
|
||
document.getElementById('download-template-btn')?.addEventListener('click', () => downloadTemplate());
|
||
document.getElementById('batch-delete-btn')?.addEventListener('click', () => batchDelete());
|
||
document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter') BOM.search();
|
||
});
|
||
loadList();
|
||
}, 100);
|
||
|
||
return html;
|
||
});
|
||
|
||
function showImportDialog() {
|
||
const input = document.createElement('input');
|
||
input.type = 'file';
|
||
input.accept = '.xlsx,.xls,.csv';
|
||
input.onchange = async (e) => {
|
||
const file = e.target.files[0];
|
||
if (!file) return;
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
try {
|
||
const overlay = document.getElementById('overlay');
|
||
overlay.classList.remove('hidden');
|
||
|
||
const res = await fetch('/api/bom/import', {
|
||
method: 'POST',
|
||
body: formData,
|
||
credentials: 'include'
|
||
});
|
||
|
||
overlay.classList.add('hidden');
|
||
|
||
const data = await res.json();
|
||
if (data.ok) {
|
||
alert(data.message || '导入成功');
|
||
loadList();
|
||
} else {
|
||
alert(data.error || '导入失败');
|
||
}
|
||
} catch (err) {
|
||
document.getElementById('overlay').classList.add('hidden');
|
||
alert('导入失败: ' + err.message);
|
||
}
|
||
};
|
||
input.click();
|
||
}
|
||
|
||
function downloadTemplate() {
|
||
const headers = ['产品编码', '产品名称', '物料编码', '物料名称', '单机用量', '单位', '最小包装', '供应商', '备注'];
|
||
const example = ['AP05', 'AP05基站', 'PCB-001', '主控PCB板', '1', 'pcs', '10', '深圳电子', ''];
|
||
|
||
const csvContent = [headers.join(','), example.join(',')].join('\n');
|
||
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
|
||
const link = document.createElement('a');
|
||
link.href = URL.createObjectURL(blob);
|
||
link.download = 'BOM导入模板.csv';
|
||
link.click();
|
||
}
|
||
|
||
async function loadList() {
|
||
try {
|
||
const res = await API.get('/api/bom');
|
||
bomList = res.list || [];
|
||
renderList();
|
||
} catch (e) {
|
||
console.error('加载BOM列表失败:', e);
|
||
document.getElementById('bom-list').innerHTML = '<tr><td colspan="11" class="text-center" style="color: var(--danger);">加载失败</td></tr>';
|
||
}
|
||
}
|
||
|
||
function renderList() {
|
||
const tbody = document.getElementById('bom-list');
|
||
const keyword = (document.getElementById('search-keyword')?.value || '').toLowerCase();
|
||
|
||
let filtered = bomList;
|
||
if (keyword) {
|
||
filtered = bomList.filter(item =>
|
||
(item.product_code || '').toLowerCase().includes(keyword) ||
|
||
(item.product_name || '').toLowerCase().includes(keyword) ||
|
||
(item.material_code || '').toLowerCase().includes(keyword) ||
|
||
(item.material_name || '').toLowerCase().includes(keyword)
|
||
);
|
||
}
|
||
|
||
const totalPages = Math.ceil(filtered.length / pageSize);
|
||
const start = (currentPage - 1) * pageSize;
|
||
const pageData = filtered.slice(start, start + pageSize);
|
||
|
||
if (pageData.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="11" class="text-center">暂无数据</td></tr>';
|
||
} else {
|
||
tbody.innerHTML = pageData.map(item => `
|
||
<tr>
|
||
<td><label class="custom-checkbox"><input type="checkbox" class="row-checkbox" data-id="${item.id}" /><span class="checkmark"></span></label></td>
|
||
<td>${escapeHtml(item.product_code || '')}</td>
|
||
<td>${escapeHtml(item.product_name || '')}</td>
|
||
<td>${escapeHtml(item.material_code || '')}</td>
|
||
<td>${escapeHtml(item.material_name || '')}</td>
|
||
<td>${item.unit_qty || 1}</td>
|
||
<td>${escapeHtml(item.unit || 'pcs')}</td>
|
||
<td>${item.min_package || 1}</td>
|
||
<td>${escapeHtml(item.supplier || '-')}</td>
|
||
<td>${escapeHtml(item.remark || '-')}</td>
|
||
<td>
|
||
<button class="btn btn-sm btn-secondary" onclick="BOM.edit(${item.id})">编辑</button>
|
||
<button class="btn btn-sm btn-danger" onclick="BOM.delete(${item.id})">删除</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
renderPagination(totalPages);
|
||
}
|
||
|
||
function renderPagination(totalPages) {
|
||
const container = document.getElementById('pagination');
|
||
if (totalPages <= 1) {
|
||
container.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
html += `<button class="btn btn-sm btn-secondary" ${currentPage <= 1 ? 'disabled' : ''} onclick="BOM.goPage(${currentPage - 1})">上一页</button>`;
|
||
html += `<span style="padding: 0 12px; line-height: 32px;">第 ${currentPage} / ${totalPages} 页</span>`;
|
||
html += `<button class="btn btn-sm btn-secondary" ${currentPage >= totalPages ? 'disabled' : ''} onclick="BOM.goPage(${currentPage + 1})">下一页</button>`;
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function openModal(item = null) {
|
||
editingId = item?.id || null;
|
||
document.getElementById('modal-title').textContent = item ? '编辑BOM' : '新增BOM';
|
||
document.getElementById('product-code').value = item?.product_code || '';
|
||
document.getElementById('product-name').value = item?.product_name || '';
|
||
document.getElementById('material-code').value = item?.material_code || '';
|
||
document.getElementById('material-name').value = item?.material_name || '';
|
||
document.getElementById('unit-qty').value = item?.unit_qty || 1;
|
||
document.getElementById('unit').value = item?.unit || 'pcs';
|
||
document.getElementById('min-package').value = item?.min_package || 1;
|
||
document.getElementById('supplier').value = item?.supplier || '';
|
||
document.getElementById('remark').value = item?.remark || '';
|
||
document.getElementById('bom-modal').style.display = 'flex';
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('bom-modal').style.display = 'none';
|
||
editingId = null;
|
||
}
|
||
|
||
async function save() {
|
||
const data = {
|
||
product_code: document.getElementById('product-code').value.trim(),
|
||
product_name: document.getElementById('product-name').value.trim(),
|
||
material_code: document.getElementById('material-code').value.trim(),
|
||
material_name: document.getElementById('material-name').value.trim(),
|
||
unit_qty: parseFloat(document.getElementById('unit-qty').value) || 1,
|
||
unit: document.getElementById('unit').value.trim() || 'pcs',
|
||
min_package: parseInt(document.getElementById('min-package').value) || 1,
|
||
supplier: document.getElementById('supplier').value.trim(),
|
||
remark: document.getElementById('remark').value.trim()
|
||
};
|
||
|
||
if (!data.product_code || !data.product_name || !data.material_code || !data.material_name) {
|
||
alert('请填写所有必填项');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (editingId) {
|
||
await API.put(`/api/bom/${editingId}`, data);
|
||
alert('更新成功');
|
||
} else {
|
||
await API.post('/api/bom', data);
|
||
alert('创建成功');
|
||
}
|
||
closeModal();
|
||
loadList();
|
||
} catch (e) {
|
||
alert(e.message || '操作失败');
|
||
}
|
||
}
|
||
|
||
async function edit(id) {
|
||
const item = bomList.find(x => x.id === id);
|
||
if (item) openModal(item);
|
||
}
|
||
|
||
async function deleteBom(id) {
|
||
if (!confirm('确定要删除这条BOM吗?')) return;
|
||
try {
|
||
await API.delete(`/api/bom/${id}`);
|
||
alert('删除成功');
|
||
loadList();
|
||
} catch (e) {
|
||
alert(e.message || '删除失败');
|
||
}
|
||
}
|
||
|
||
async function batchDelete() {
|
||
const checked = document.querySelectorAll('.row-checkbox:checked');
|
||
if (checked.length === 0) {
|
||
alert('请先选择要删除的项');
|
||
return;
|
||
}
|
||
if (!confirm(`确定要删除选中的 ${checked.length} 条BOM吗?`)) return;
|
||
|
||
const ids = Array.from(checked).map(cb => parseInt(cb.dataset.id));
|
||
try {
|
||
await API.post('/api/bom/batch-delete', { ids });
|
||
alert('批量删除成功');
|
||
loadList();
|
||
} catch (e) {
|
||
alert(e.message || '批量删除失败');
|
||
}
|
||
}
|
||
|
||
function toggleSelectAll(checkbox) {
|
||
document.querySelectorAll('.row-checkbox').forEach(cb => cb.checked = checkbox.checked);
|
||
}
|
||
|
||
function search() {
|
||
currentPage = 1;
|
||
renderList();
|
||
}
|
||
|
||
function resetSearch() {
|
||
document.getElementById('search-keyword').value = '';
|
||
currentPage = 1;
|
||
renderList();
|
||
}
|
||
|
||
function goPage(page) {
|
||
currentPage = page;
|
||
renderList();
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
if (!str) return '';
|
||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||
}
|
||
|
||
window.BOM = {
|
||
search,
|
||
resetSearch,
|
||
edit,
|
||
delete: deleteBom,
|
||
closeModal,
|
||
save,
|
||
toggleSelectAll,
|
||
goPage
|
||
};
|
||
})();
|