ERP/frontend/js/components/initial-stock.js

641 lines
25 KiB
JavaScript
Raw Normal View History

2025-12-08 03:20:47 +00:00
// 期初库存管理
(() => {
let stockList = [];
let currentPage = 1;
const pageSize = 20;
let editingId = null;
2026-03-16 05:38:29 +00:00
let currentFactory = '';
2025-12-08 03:20:47 +00:00
Router.register('/plan-mgmt/initial-stock', async () => {
const html = `
<style>
#initial-stock-page {
padding: 20px;
background: var(--bg);
}
#initial-stock-page .page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
#initial-stock-page h1 {
margin: 0;
font-size: 24px;
}
#initial-stock-page .content-area {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
#initial-stock-page .filter-section {
padding: 20px;
display: flex;
gap: 15px;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border);
}
#initial-stock-page .table-wrapper {
overflow-x: auto;
}
#initial-stock-page table {
width: 100%;
border-collapse: collapse;
margin: 0;
}
#initial-stock-page thead {
background: var(--border);
position: sticky;
top: 0;
z-index: 10;
}
#initial-stock-page th {
padding: 8px 12px;
text-align: left;
font-weight: 600;
color: var(--text);
border-bottom: 2px solid var(--border);
font-size: 14px;
white-space: nowrap;
}
#initial-stock-page td {
padding: 8px 12px;
border-bottom: 1px solid var(--border);
color: var(--text);
font-size: 14px;
}
#initial-stock-page tbody tr:hover {
background: var(--hover);
}
#initial-stock-page .text-center {
text-align: center;
}
</style>
<div id="initial-stock-page">
<div class="page-header">
<h1 style="margin: 0; font-size: 24px;">期初库存管理</h1>
<div class="page-actions" style="margin-top: 16px;">
<button id="add-stock-btn" class="btn btn-primary" style="margin-right: 10px;">新增库存</button>
<button id="import-stock-btn" class="btn btn-secondary" style="margin-right: 10px;">导入 Excel</button>
<button id="download-template-btn" class="btn btn-secondary" style="margin-right: 10px;">下载模板</button>
<button id="batch-delete-btn" class="btn btn-danger">批量删除</button>
</div>
2025-12-08 03:20:47 +00:00
</div>
<div class="content-area">
<div class="filter-section" style="padding: 20px; display: flex; gap: 15px; align-items: center; justify-content: flex-start;">
2026-03-16 05:38:29 +00:00
<select id="factory-filter" class="input" style="width: 200px;" onchange="InitialStock.filterByFactory()">
<option value="">全部工厂</option>
</select>
<input type="text" id="search-keyword" class="input" placeholder="搜索物料编码、名称或供应商" style="flex: 1; max-width: 400px;" />
<button class="btn btn-secondary" onclick="InitialStock.search()">搜索</button>
<button class="btn btn-secondary" onclick="InitialStock.resetSearch()">重置</button>
2025-12-08 03:20:47 +00:00
</div>
<div class="table-wrapper">
<table>
2025-12-08 03:20:47 +00:00
<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>
2026-03-16 05:38:29 +00:00
<th>工厂</th>
2025-12-08 03:20:47 +00:00
<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="padding: 20px; display: flex; justify-content: center; gap: 8px;"></div>
2025-12-08 03:20:47 +00:00
</div>
</div>
2026-03-16 05:38:29 +00:00
<!-- Excel导入弹窗 -->
<div id="import-excel-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 600px;">
<div class="modal-header">
<h2>导入期初库存清单</h2>
<button class="modal-close" onclick="InitialStock.closeImportModal()">&times;</button>
</div>
<div class="modal-body">
<div class="field">
<label>默认工厂可选</label>
<div style="display: flex; gap: 5px;">
<input type="text" id="default-factory" class="input" placeholder="不填写则使用Excel中的工厂值" style="flex: 1;" />
<select id="default-factory-select" onchange="document.getElementById('default-factory').value = this.value;" style="width: 120px;">
<option value="">快速选择</option>
</select>
</div>
<div style="margin-top: 4px; font-size: 12px; color: var(--text-3);">
如果Excel中工厂列为空将使用此默认值如果都未填写则标记为"未指定"
</div>
</div>
<div class="field">
<label>选择Excel文件 <span style="color: var(--danger);">*</span></label>
<input type="file" id="excel-file" class="input" accept=".xlsx,.xls,.csv" />
<div style="margin-top: 8px; font-size: 12px; color: var(--text-3);">
支持 .xlsx.xls .csv 格式文件大小不超过 10MB
</div>
</div>
<div style="margin-top: 16px; padding: 12px; background: var(--surface); border-radius: 8px; border: 1px solid var(--border);">
<div style="font-size: 14px; color: var(--text-2); margin-bottom: 12px;">
<strong>Excel文件格式要求</strong>
</div>
<table style="width: 100%; font-size: 13px; border-collapse: collapse;">
<thead>
<tr style="background: var(--surface-2);">
<th style="padding: 8px; text-align: left; border: 1px solid var(--border);">列名</th>
<th style="padding: 8px; text-align: left; border: 1px solid var(--border);">说明</th>
<th style="padding: 8px; text-align: left; border: 1px solid var(--border);">必填</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">物料编码</td>
<td style="padding: 6px; border: 1px solid var(--border;">物料编码</td>
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;"></td>
</tr>
<tr>
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">物料名称</td>
<td style="padding: 6px; border: 1px solid var(--border;">物料名称</td>
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;"></td>
</tr>
<tr>
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">库存数量</td>
<td style="padding: 6px; border: 1px solid var(--border;">当前库存数量</td>
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;"></td>
</tr>
<tr>
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">单位</td>
<td style="padding: 6px; border: 1px solid var(--border;">单位pcs</td>
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;"></td>
</tr>
<tr>
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">最小包装</td>
<td style="padding: 6px; border: 1px solid var(--border;">最小包装数量</td>
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;"></td>
</tr>
<tr>
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">供应商</td>
<td style="padding: 6px; border: 1px solid var(--border;">供应商名称</td>
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;"></td>
</tr>
<tr>
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">工厂</td>
<td style="padding: 6px; border: 1px solid var(--border;">工厂名称可输入自定义名称友辉铨宝</td>
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;"></td>
</tr>
<tr>
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">备注</td>
<td style="padding: 6px; border: 1px solid var(--border;">备注信息</td>
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;"></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="InitialStock.closeImportModal()">取消</button>
<button class="btn btn-primary" onclick="InitialStock.importExcel()">导入</button>
</div>
</div>
</div>
2025-12-08 03:20:47 +00:00
<!-- 新增/编辑库存弹窗 -->
<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()">&times;</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>
2026-03-16 05:38:29 +00:00
<div class="field">
<label>工厂</label>
<div style="display: flex; gap: 5px;">
<input type="text" id="factory" class="input" placeholder="请输入工厂名称" style="flex: 1;" />
<select id="factory-select" onchange="document.getElementById('factory').value = this.value;" style="width: 120px;">
<option value="">快速选择</option>
</select>
</div>
</div>
2025-12-08 03:20:47 +00:00
<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();
2026-03-16 05:38:29 +00:00
loadFactories();
2025-12-08 03:20:47 +00:00
}, 100);
return html;
});
2026-03-16 05:38:29 +00:00
async function showImportDialog() {
document.getElementById('excel-file').value = '';
document.getElementById('default-factory').value = '';
document.getElementById('import-excel-modal').style.display = 'flex';
// 动态加载工厂列表
await loadFactoriesForSelect('default-factory-select');
}
function closeImportModal() {
document.getElementById('import-excel-modal').style.display = 'none';
}
async function importExcel() {
const fileInput = document.getElementById('excel-file');
const file = fileInput.files[0];
const defaultFactory = document.getElementById('default-factory').value.trim();
if (!file) {
alert('请选择要导入的文件');
return;
}
const formData = new FormData();
formData.append('file', file);
if (defaultFactory) {
formData.append('default_factory', defaultFactory);
}
try {
const overlay = document.getElementById('overlay');
overlay.classList.remove('hidden');
2025-12-08 03:20:47 +00:00
2026-03-16 05:38:29 +00:00
const res = await fetch('/api/initial-stock/import', {
method: 'POST',
body: formData,
credentials: 'include'
});
2025-12-08 03:20:47 +00:00
2026-03-16 05:38:29 +00:00
overlay.classList.add('hidden');
const data = await res.json();
if (data.ok) {
alert(data.message || '导入成功');
closeImportModal();
loadList();
loadFactories(); // 重新加载工厂列表
} else {
alert(data.error || '导入失败');
}
} catch (err) {
document.getElementById('overlay').classList.add('hidden');
alert('导入失败: ' + err.message);
}
}
async function loadFactories() {
try {
const res = await API.get('/api/factories');
const factories = res.factories || [];
const select = document.getElementById('factory-filter');
if (select) {
// 保留"全部工厂"选项,清空其他选项
select.innerHTML = '<option value="">全部工厂</option>';
factories.forEach(factory => {
const option = document.createElement('option');
option.value = factory;
option.textContent = factory;
select.appendChild(option);
2025-12-08 03:20:47 +00:00
});
2026-03-16 05:38:29 +00:00
}
} catch (e) {
console.error('加载工厂列表失败:', e);
}
}
async function loadFactoriesForSelect(selectId) {
try {
const res = await API.get('/api/factories');
const factories = res.factories || [];
const select = document.getElementById(selectId);
if (select) {
const currentValue = select.value;
const firstOption = select.options[0] ? select.options[0].outerHTML : '<option value="">快速选择</option>';
select.innerHTML = firstOption;
factories.forEach(factory => {
const option = document.createElement('option');
option.value = factory;
option.textContent = factory;
select.appendChild(option);
});
if (currentValue) {
select.value = currentValue;
2025-12-08 03:20:47 +00:00
}
}
2026-03-16 05:38:29 +00:00
} catch (e) {
console.error('加载工厂列表失败:', e);
}
2025-12-08 03:20:47 +00:00
}
function downloadTemplate() {
2026-03-16 05:38:29 +00:00
const headers = ['物料编码', '物料名称', '库存数量', '单位', '最小包装', '供应商', '工厂', '备注'];
const example1 = ['PCB-001', '主控PCB板', '50', 'pcs', '10', '深圳电子', '友辉', ''];
const example2 = ['PCB-002', '电源板', '100', 'pcs', '20', '东莞电子', '铨宝', ''];
const example3 = ['PCB-003', '显示板', '75', 'pcs', '15', '苏州电子', '新工厂', ''];
2025-12-08 03:20:47 +00:00
2026-03-16 05:38:29 +00:00
const csvContent = [headers.join(','), example1.join(','), example2.join(','), example3.join(',')].join('\n');
2025-12-08 03:20:47 +00:00
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 {
2026-03-16 05:38:29 +00:00
let url = '/api/initial-stock';
if (currentFactory) {
url += `?factory=${encodeURIComponent(currentFactory)}`;
}
const res = await API.get(url);
2025-12-08 03:20:47 +00:00
stockList = res.list || [];
renderList();
} catch (e) {
console.error('加载库存列表失败:', e);
2026-03-16 05:38:29 +00:00
document.getElementById('stock-list').innerHTML = '<tr><td colspan="11" class="text-center" style="color: var(--danger);">加载失败</td></tr>';
2025-12-08 03:20:47 +00:00
}
}
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) ||
(item.supplier || '').toLowerCase().includes(keyword)
2025-12-08 03:20:47 +00:00
);
}
const totalPages = Math.ceil(filtered.length / pageSize);
const start = (currentPage - 1) * pageSize;
const pageData = filtered.slice(start, start + pageSize);
if (pageData.length === 0) {
2026-03-16 05:38:29 +00:00
tbody.innerHTML = '<tr><td colspan="11" class="text-center">暂无数据</td></tr>';
2025-12-08 03:20:47 +00:00
} else {
tbody.innerHTML = pageData.map(item => `
<tr>
<td><label class="custom-checkbox"><input type="checkbox" class="row-checkbox" data-id="${item.id}" onchange="InitialStock.updateSelectAll()" /><span class="checkmark"></span></label></td>
2025-12-08 03:20:47 +00:00
<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>
2026-03-16 05:38:29 +00:00
<td>${escapeHtml(item.factory || '-')}</td>
2025-12-08 03:20:47 +00:00
<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;
}
2026-03-16 05:38:29 +00:00
async function openModal(item = null) {
2025-12-08 03:20:47 +00:00
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 || '';
2026-03-16 05:38:29 +00:00
document.getElementById('factory').value = item?.factory || '';
2025-12-08 03:20:47 +00:00
document.getElementById('remark').value = item?.remark || '';
document.getElementById('stock-modal').style.display = 'flex';
2026-03-16 05:38:29 +00:00
// 动态加载工厂列表
await loadFactoriesForSelect('factory-select');
2025-12-08 03:20:47 +00:00
}
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(),
2026-03-16 05:38:29 +00:00
factory: document.getElementById('factory').value.trim(),
2025-12-08 03:20:47 +00:00
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 updateSelectAll() {
const selectAllCheckbox = document.getElementById('select-all');
const rowCheckboxes = document.querySelectorAll('.row-checkbox');
selectAllCheckbox.checked = rowCheckboxes.length > 0 && Array.from(rowCheckboxes).every(cb => cb.checked);
}
2025-12-08 03:20:47 +00:00
function search() {
currentPage = 1;
renderList();
}
function resetSearch() {
document.getElementById('search-keyword').value = '';
2026-03-16 05:38:29 +00:00
document.getElementById('factory-filter').value = '';
currentFactory = '';
2025-12-08 03:20:47 +00:00
currentPage = 1;
2026-03-16 05:38:29 +00:00
loadList();
}
function filterByFactory() {
const factory = document.getElementById('factory-filter')?.value || '';
currentFactory = factory;
currentPage = 1;
loadList();
2025-12-08 03:20:47 +00:00
}
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
window.InitialStock = {
search,
resetSearch,
2026-03-16 05:38:29 +00:00
filterByFactory,
2025-12-08 03:20:47 +00:00
edit,
delete: deleteStock,
closeModal,
save,
toggleSelectAll,
2026-03-16 05:38:29 +00:00
updateSelectAll,
goPage,
closeImportModal,
importExcel
2025-12-08 03:20:47 +00:00
};
})();