ERP/frontend/js/components/bom.js
2025-12-08 11:20:47 +08:00

370 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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()">&times;</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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
window.BOM = {
search,
resetSearch,
edit,
delete: deleteBom,
closeModal,
save,
toggleSelectAll,
goPage
};
})();