// 期初库存管理 (() => { let stockList = []; let currentPage = 1; const pageSize = 20; let editingId = null; let currentFactory = ''; Router.register('/plan-mgmt/initial-stock', async () => { const html = `
物料编码 物料名称 库存数量 单位 最小包装 供应商 工厂 备注 更新时间 操作
加载中...
`; 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('restore-backup-btn')?.addEventListener('click', () => showRestoreDialog()); document.getElementById('batch-delete-btn')?.addEventListener('click', () => batchDelete()); document.getElementById('search-keyword')?.addEventListener('keypress', (e) => { if (e.key === 'Enter') InitialStock.search(); }); document.addEventListener('click', (e) => { const exportMenu = document.getElementById('export-menu'); const exportBtn = document.getElementById('export-btn'); if (exportMenu && !exportMenu.contains(e.target) && e.target !== exportBtn) { exportMenu.style.display = 'none'; } }); loadList(); loadFactories(); }, 100); return html; }); 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'); 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 || '导入成功'); 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 = ''; factories.forEach(factory => { const option = document.createElement('option'); option.value = factory; option.textContent = factory; select.appendChild(option); }); } } 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 : ''; 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; } } } catch (e) { console.error('加载工厂列表失败:', e); } } function downloadTemplate() { const headers = ['物料编码', '物料名称', '库存数量', '单位', '最小包装', '供应商', '工厂', '备注']; const example1 = ['PCB-001', '主控PCB板', '50', 'pcs', '10', '深圳电子', '友辉', '']; const example2 = ['PCB-002', '电源板', '100', 'pcs', '20', '东莞电子', '铨宝', '']; const example3 = ['PCB-003', '显示板', '75', 'pcs', '15', '苏州电子', '新工厂', '']; const csvContent = [headers.join(','), example1.join(','), example2.join(','), example3.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 { let url = '/api/initial-stock'; if (currentFactory) { url += `?factory=${encodeURIComponent(currentFactory)}`; } const res = await API.get(url); stockList = res.list || []; renderList(); } catch (e) { console.error('加载库存列表失败:', e); document.getElementById('stock-list').innerHTML = '加载失败'; } } 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) ); } 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 = '暂无数据'; } else { tbody.innerHTML = pageData.map(item => ` ${escapeHtml(item.material_code || '')} ${escapeHtml(item.material_name || '')} ${item.stock_qty || 0} ${escapeHtml(item.unit || 'pcs')} ${item.min_package || 1} ${escapeHtml(item.supplier || '-')} ${escapeHtml(item.factory || '-')} ${escapeHtml(item.remark || '-')} ${formatTime(item.updated_at)} `).join(''); } renderPagination(totalPages); } function renderPagination(totalPages) { const container = document.getElementById('pagination'); if (totalPages <= 1) { container.innerHTML = ''; return; } let html = ''; html += ``; html += `第 ${currentPage} / ${totalPages} 页`; html += ``; container.innerHTML = html; } async 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('factory').value = item?.factory || ''; document.getElementById('remark').value = item?.remark || ''; document.getElementById('stock-modal').style.display = 'flex'; // 动态加载工厂列表 await loadFactoriesForSelect('factory-select'); } 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(), factory: document.getElementById('factory').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 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); } function search() { currentPage = 1; renderList(); } function resetSearch() { document.getElementById('search-keyword').value = ''; document.getElementById('factory-filter').value = ''; currentFactory = ''; currentPage = 1; loadList(); } function filterByFactory() { const factory = document.getElementById('factory-filter')?.value || ''; currentFactory = factory; currentPage = 1; loadList(); } 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, '"'); } function toggleExportMenu(event) { event.stopPropagation(); const menu = document.getElementById('export-menu'); menu.style.display = menu.style.display === 'none' ? 'block' : 'none'; } async function ensureXLSXLoaded() { if (typeof XLSX === 'undefined') { try { await loadXLSXLibrary(); } catch (e) { throw new Error('无法加载 Excel 库,请刷新页面重试'); } } } function loadXLSXLibrary() { return new Promise((resolve, reject) => { if (typeof XLSX !== 'undefined') { resolve(); return; } const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js'; script.onload = () => resolve(); script.onerror = () => reject(new Error('加载 XLSX 库失败')); document.head.appendChild(script); }); } async function exportAll() { try { document.getElementById('export-menu').style.display = 'none'; if (stockList.length === 0) { alert('没有数据可导出'); return; } await ensureXLSXLoaded(); const exportData = stockList.map(item => ({ '物料编码': item.material_code || '', '物料名称': item.material_name || '', '库存数量': item.stock_qty || 0, '单位': item.unit || 'pcs', '最小包装': item.min_package || 1, '供应商': item.supplier || '', '工厂': item.factory || '', '备注': item.remark || '', '更新时间': formatTime(item.updated_at) })); const ws = XLSX.utils.json_to_sheet(exportData); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, '期初库存'); const colWidths = [ { wch: 15 }, { wch: 25 }, { wch: 10 }, { wch: 8 }, { wch: 10 }, { wch: 20 }, { wch: 15 }, { wch: 30 }, { wch: 18 } ]; ws['!cols'] = colWidths; const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); const filename = `期初库存_全部_${timestamp}.xlsx`; XLSX.writeFile(wb, filename); console.log(`成功导出 ${exportData.length} 条数据到 ${filename}`); } catch (err) { console.error('导出失败:', err); alert('导出失败: ' + err.message); } } async function exportSelected() { try { document.getElementById('export-menu').style.display = 'none'; const checked = document.querySelectorAll('.row-checkbox:checked'); if (checked.length === 0) { alert('请先选择要导出的数据'); return; } const selectedIds = Array.from(checked).map(cb => parseInt(cb.dataset.id)); const selectedData = stockList.filter(item => selectedIds.includes(item.id)); await ensureXLSXLoaded(); const exportData = selectedData.map(item => ({ '物料编码': item.material_code || '', '物料名称': item.material_name || '', '库存数量': item.stock_qty || 0, '单位': item.unit || 'pcs', '最小包装': item.min_package || 1, '供应商': item.supplier || '', '工厂': item.factory || '', '备注': item.remark || '', '更新时间': formatTime(item.updated_at) })); const ws = XLSX.utils.json_to_sheet(exportData); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, '期初库存'); const colWidths = [ { wch: 15 }, { wch: 25 }, { wch: 10 }, { wch: 8 }, { wch: 10 }, { wch: 20 }, { wch: 15 }, { wch: 30 }, { wch: 18 } ]; ws['!cols'] = colWidths; const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); const filename = `期初库存_选中${exportData.length}条_${timestamp}.xlsx`; XLSX.writeFile(wb, filename); console.log(`成功导出 ${exportData.length} 条数据到 ${filename}`); } catch (err) { console.error('导出失败:', err); alert('导出失败: ' + err.message); } } async function exportByFactory() { try { document.getElementById('export-menu').style.display = 'none'; const factory = document.getElementById('factory-filter')?.value || ''; if (!factory) { alert('请先选择要导出的工厂'); return; } const factoryData = stockList.filter(item => item.factory === factory); if (factoryData.length === 0) { alert(`工厂 "${factory}" 没有数据可导出`); return; } await ensureXLSXLoaded(); const exportData = factoryData.map(item => ({ '物料编码': item.material_code || '', '物料名称': item.material_name || '', '库存数量': item.stock_qty || 0, '单位': item.unit || 'pcs', '最小包装': item.min_package || 1, '供应商': item.supplier || '', '工厂': item.factory || '', '备注': item.remark || '', '更新时间': formatTime(item.updated_at) })); const ws = XLSX.utils.json_to_sheet(exportData); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, '期初库存'); const colWidths = [ { wch: 15 }, { wch: 25 }, { wch: 10 }, { wch: 8 }, { wch: 10 }, { wch: 20 }, { wch: 15 }, { wch: 30 }, { wch: 18 } ]; ws['!cols'] = colWidths; const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); const filename = `期初库存_${factory}_${timestamp}.xlsx`; XLSX.writeFile(wb, filename); console.log(`成功导出 ${exportData.length} 条数据到 ${filename}`); } catch (err) { console.error('导出失败:', err); alert('导出失败: ' + err.message); } } async function showRestoreDialog() { document.getElementById('restore-modal').style.display = 'flex'; await loadBackupList(); } function closeRestoreModal() { document.getElementById('restore-modal').style.display = 'none'; } async function loadBackupList() { const container = document.getElementById('backup-list'); try { const res = await API.get('/api/backups?table_name=initial_stock'); const backups = res.list || []; if (backups.length === 0) { container.innerHTML = '
暂无备份记录
'; return; } container.innerHTML = backups.map(backup => `
${getOperationText(backup.operation)} - ${backup.factory || '全部工厂'}
📦 记录数量: ${backup.record_count}
👤 操作人: ${backup.created_by || '系统'}
🕐 备份时间: ${formatTime(backup.created_at)}
${backup.description ? `
💬 ${escapeHtml(backup.description)}
` : ''}
`).join(''); } catch (e) { container.innerHTML = `
加载失败: ${e.message}
`; } } function getOperationText(operation) { const map = { 'import': '📥 导入前备份', 'batch_delete': '🗑️ 批量删除前备份', 'restore': '🔄 恢复前备份', 'manual': '💾 手动备份' }; return map[operation] || operation; } async function restoreBackup(backupId) { if (!confirm('确定要恢复此备份吗?\n\n当前数据将被完全替换,但会自动备份当前数据。')) { return; } try { const overlay = document.getElementById('overlay'); overlay.classList.remove('hidden'); const res = await API.post(`/api/backups/${backupId}/restore`, {}); overlay.classList.add('hidden'); if (res.ok) { alert(res.message || '数据恢复成功'); closeRestoreModal(); loadList(); } else { alert(res.error || '恢复失败'); } } catch (e) { document.getElementById('overlay').classList.add('hidden'); alert('恢复失败: ' + e.message); } } async function deleteBackup(backupId) { if (!confirm('确定要删除此备份吗?此操作不可恢复。')) { return; } try { await API.delete(`/api/backups/${backupId}`); alert('备份已删除'); await loadBackupList(); } catch (e) { alert('删除失败: ' + e.message); } } window.InitialStock = { search, resetSearch, filterByFactory, edit, delete: deleteStock, closeModal, save, toggleSelectAll, updateSelectAll, goPage, closeImportModal, importExcel, closeRestoreModal, restoreBackup, deleteBackup, toggleExportMenu, exportAll, exportSelected, exportByFactory }; })();