366 lines
13 KiB
JavaScript
366 lines
13 KiB
JavaScript
// 期初库存管理
|
|
(() => {
|
|
let stockList = [];
|
|
let currentPage = 1;
|
|
const pageSize = 20;
|
|
let editingId = null;
|
|
|
|
Router.register('/plan-mgmt/initial-stock', async () => {
|
|
const html = `
|
|
<div class="page-header">
|
|
<h1>期初库存管理</h1>
|
|
<div class="page-actions">
|
|
<button id="add-stock-btn" class="btn btn-primary">新增库存</button>
|
|
<button id="import-stock-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="InitialStock.search()">搜索</button>
|
|
<button class="btn btn-secondary" onclick="InitialStock.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="InitialStock.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>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="stock-list">
|
|
<tr><td colspan="10" 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>
|
|
|
|
<!-- 新增/编辑库存弹窗 -->
|
|
<div id="stock-modal" class="modal" style="display:none;">
|
|
<div class="modal-content" style="max-width: 500px;">
|
|
<div class="modal-header">
|
|
<h2 id="modal-title">新增期初库存</h2>
|
|
<button class="modal-close" onclick="InitialStock.closeModal()">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="stock-form">
|
|
<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 style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
|
|
<div class="field">
|
|
<label>库存数量 <span style="color: var(--danger);">*</span></label>
|
|
<input type="number" id="stock-qty" class="input" min="0" value="0" required />
|
|
</div>
|
|
<div class="field">
|
|
<label>单位</label>
|
|
<input type="text" id="unit" class="input" value="pcs" />
|
|
</div>
|
|
</div>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
|
|
<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">
|
|
<label>备注</label>
|
|
<textarea id="remark" class="input" rows="2"></textarea>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" onclick="InitialStock.closeModal()">取消</button>
|
|
<button class="btn btn-primary" onclick="InitialStock.save()">保存</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
setTimeout(() => {
|
|
document.getElementById('add-stock-btn')?.addEventListener('click', () => openModal());
|
|
document.getElementById('import-stock-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') InitialStock.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/initial-stock/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 = ['PCB-001', '主控PCB板', '50', '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 = '期初库存导入模板.csv';
|
|
link.click();
|
|
}
|
|
|
|
async function loadList() {
|
|
try {
|
|
const res = await API.get('/api/initial-stock');
|
|
stockList = res.list || [];
|
|
renderList();
|
|
} catch (e) {
|
|
console.error('加载库存列表失败:', e);
|
|
document.getElementById('stock-list').innerHTML = '<tr><td colspan="10" class="text-center" style="color: var(--danger);">加载失败</td></tr>';
|
|
}
|
|
}
|
|
|
|
function renderList() {
|
|
const tbody = document.getElementById('stock-list');
|
|
const keyword = (document.getElementById('search-keyword')?.value || '').toLowerCase();
|
|
|
|
let filtered = stockList;
|
|
if (keyword) {
|
|
filtered = stockList.filter(item =>
|
|
(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="10" 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.material_code || '')}</td>
|
|
<td>${escapeHtml(item.material_name || '')}</td>
|
|
<td style="font-weight: 600; color: var(--primary);">${item.stock_qty || 0}</td>
|
|
<td>${escapeHtml(item.unit || 'pcs')}</td>
|
|
<td>${item.min_package || 1}</td>
|
|
<td>${escapeHtml(item.supplier || '-')}</td>
|
|
<td>${escapeHtml(item.remark || '-')}</td>
|
|
<td>${formatTime(item.updated_at)}</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-secondary" onclick="InitialStock.edit(${item.id})">编辑</button>
|
|
<button class="btn btn-sm btn-danger" onclick="InitialStock.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="InitialStock.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="InitialStock.goPage(${currentPage + 1})">下一页</button>`;
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function openModal(item = null) {
|
|
editingId = item?.id || null;
|
|
document.getElementById('modal-title').textContent = item ? '编辑期初库存' : '新增期初库存';
|
|
document.getElementById('material-code').value = item?.material_code || '';
|
|
document.getElementById('material-name').value = item?.material_name || '';
|
|
document.getElementById('stock-qty').value = item?.stock_qty || 0;
|
|
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('stock-modal').style.display = 'flex';
|
|
}
|
|
|
|
function closeModal() {
|
|
document.getElementById('stock-modal').style.display = 'none';
|
|
editingId = null;
|
|
}
|
|
|
|
async function save() {
|
|
const data = {
|
|
material_code: document.getElementById('material-code').value.trim(),
|
|
material_name: document.getElementById('material-name').value.trim(),
|
|
stock_qty: parseInt(document.getElementById('stock-qty').value) || 0,
|
|
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.material_code || !data.material_name) {
|
|
alert('请填写物料编码和物料名称');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (editingId) {
|
|
await API.put(`/api/initial-stock/${editingId}`, data);
|
|
alert('更新成功');
|
|
} else {
|
|
await API.post('/api/initial-stock', data);
|
|
alert('创建成功');
|
|
}
|
|
closeModal();
|
|
loadList();
|
|
} catch (e) {
|
|
alert(e.message || '操作失败');
|
|
}
|
|
}
|
|
|
|
async function edit(id) {
|
|
const item = stockList.find(x => x.id === id);
|
|
if (item) openModal(item);
|
|
}
|
|
|
|
async function deleteStock(id) {
|
|
if (!confirm('确定要删除这条库存记录吗?')) return;
|
|
try {
|
|
await API.delete(`/api/initial-stock/${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} 条库存记录吗?`)) return;
|
|
|
|
const ids = Array.from(checked).map(cb => parseInt(cb.dataset.id));
|
|
try {
|
|
await API.post('/api/initial-stock/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 formatTime(ts) {
|
|
if (!ts) return '-';
|
|
try {
|
|
const d = new Date(ts);
|
|
return d.toLocaleString('zh-CN', { hour12: false });
|
|
} catch {
|
|
return ts;
|
|
}
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
|
|
window.InitialStock = {
|
|
search,
|
|
resetSearch,
|
|
edit,
|
|
delete: deleteStock,
|
|
closeModal,
|
|
save,
|
|
toggleSelectAll,
|
|
goPage
|
|
};
|
|
})();
|