(() => { Router.register('/plan-mgmt/material-purchase', async () => { return `
标题 生产计划明细物料需求清单编号 生产计划编号 产品BOM分析结果 状态 需求状态 物料齐套率 物料编码 物料名称 物料批次号 物料层级 所需物料数 库存现有物料数 欠缺值 物料获取方式 实时库存值 待入库数量 派发数量 入库数量 提交人 提交时间 更新时间 操作
暂无数据
`; }); window.MaterialPurchase = { currentPage: 1, pageSize: 20, totalPages: 1, data: [], editingId: null, selectedIds: new Set(), recycleBin: [], init() { this.loadFromStorage(); this.loadData(); this.bindEvents(); }, // 从 localStorage 加载数据 loadFromStorage() { try { const savedData = localStorage.getItem('materialPurchaseData'); const savedRecycleBin = localStorage.getItem('materialPurchaseRecycleBin'); if (savedData) { this.data = JSON.parse(savedData); } if (savedRecycleBin) { this.recycleBin = JSON.parse(savedRecycleBin); } } catch (error) { console.error('加载本地数据失败:', error); } }, // 保存到 localStorage saveToStorage() { try { localStorage.setItem('materialPurchaseData', JSON.stringify(this.data)); localStorage.setItem('materialPurchaseRecycleBin', JSON.stringify(this.recycleBin)); } catch (error) { console.error('保存本地数据失败:', error); } }, bindEvents() { // 自动计算欠缺值 const requiredQty = document.getElementById('form-required-qty'); const stockQty = document.getElementById('form-stock-qty'); const shortage = document.getElementById('form-shortage'); const calculateShortage = () => { const required = parseFloat(requiredQty?.value || 0); const stock = parseFloat(stockQty?.value || 0); if (shortage) { shortage.value = Math.max(0, required - stock); } }; requiredQty?.addEventListener('input', calculateShortage); stockQty?.addEventListener('input', calculateShortage); }, async loadData() { try { const response = await fetch('/api/material-purchase/list', { credentials: 'include' }); const result = await response.json(); this.data = result.list || []; // 加载回收站数据 const recycleBinResponse = await fetch('/api/material-purchase/recycle-bin', { credentials: 'include' }); const recycleBinResult = await recycleBinResponse.json(); this.recycleBin = recycleBinResult.list || []; this.renderTable(); this.updatePagination(); } catch (error) { API.toast('加载数据失败: ' + error.message, 'error'); } }, generateMockData() { // 生成模拟数据 const mockData = []; for (let i = 1; i <= 5; i++) { mockData.push({ id: i, title: `物料需求计划-${i}`, listNo: `MRL-2024-${String(i).padStart(4, '0')}`, planNo: `PLAN-2024-${String(i).padStart(4, '0')}`, bomResult: `BOM分析-${i}`, status: ['pending', 'processing', 'completed'][i % 3], demandStatus: ['urgent', 'normal', 'low'][i % 3], completeRate: (Math.random() * 100).toFixed(2), materialCode: `MAT-${String(i).padStart(5, '0')}`, materialName: `物料名称-${i}`, batchNo: `BATCH-${String(i).padStart(4, '0')}`, level: Math.floor(Math.random() * 3) + 1, requiredQty: Math.floor(Math.random() * 1000) + 100, stockQty: Math.floor(Math.random() * 500), shortage: 0, acquireMethod: ['purchase', 'production', 'transfer'][i % 3], realtimeStock: Math.floor(Math.random() * 500), pendingQty: Math.floor(Math.random() * 200), dispatchedQty: Math.floor(Math.random() * 100), receivedQty: Math.floor(Math.random() * 150), submitter: `用户${i}`, submitTime: new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000).toISOString(), updateTime: new Date().toISOString() }); mockData[i - 1].shortage = Math.max(0, mockData[i - 1].requiredQty - mockData[i - 1].stockQty); } return mockData; }, renderTable() { const tbody = document.getElementById('material-tbody'); if (!tbody) return; if (this.data.length === 0) { tbody.innerHTML = `
暂无数据
`; return; } const start = (this.currentPage - 1) * this.pageSize; const end = start + this.pageSize; const pageData = this.data.slice(start, end); tbody.innerHTML = pageData.map(item => ` ${this.escapeHtml(item.title)} ${this.escapeHtml(item.listNo)} ${this.escapeHtml(item.planNo)} ${this.escapeHtml(item.bomResult)} ${this.getStatusText(item.status)} ${this.getDemandStatusText(item.demandStatus)} ${item.completeRate}% ${this.escapeHtml(item.materialCode)} ${this.escapeHtml(item.materialName)} ${this.escapeHtml(item.batchNo)} ${item.level} ${item.requiredQty} ${item.stockQty} ${item.shortage} ${this.getAcquireMethodText(item.acquireMethod)} ${item.realtimeStock} ${item.pendingQty} ${item.dispatchedQty} ${item.receivedQty} ${this.escapeHtml(item.submitter)} ${this.formatDateTime(item.submitTime)} ${this.formatDateTime(item.updateTime)} `).join(''); // 更新全选框状态 this.updateSelectAllCheckbox(); }, updatePagination() { this.totalPages = Math.ceil(this.data.length / this.pageSize); const pageInfo = document.getElementById('page-info'); const prevBtn = document.getElementById('prev-btn'); const nextBtn = document.getElementById('next-btn'); if (pageInfo) { pageInfo.textContent = `第 ${this.currentPage} 页 / 共 ${this.totalPages} 页`; } if (prevBtn) { prevBtn.disabled = this.currentPage === 1; } if (nextBtn) { nextBtn.disabled = this.currentPage === this.totalPages || this.totalPages === 0; } }, prevPage() { if (this.currentPage > 1) { this.currentPage--; this.renderTable(); this.updatePagination(); } }, nextPage() { if (this.currentPage < this.totalPages) { this.currentPage++; this.renderTable(); this.updatePagination(); } }, search() { const keyword = document.getElementById('search-keyword')?.value.toLowerCase() || ''; const status = document.getElementById('filter-status')?.value || ''; const demandStatus = document.getElementById('filter-demand-status')?.value || ''; this.data = this.generateMockData().filter(item => { const matchKeyword = !keyword || item.title.toLowerCase().includes(keyword) || item.listNo.toLowerCase().includes(keyword) || item.planNo.toLowerCase().includes(keyword) || item.materialCode.toLowerCase().includes(keyword); const matchStatus = !status || item.status === status; const matchDemandStatus = !demandStatus || item.demandStatus === demandStatus; return matchKeyword && matchStatus && matchDemandStatus; }); this.currentPage = 1; this.renderTable(); this.updatePagination(); }, resetFilter() { document.getElementById('search-keyword').value = ''; document.getElementById('filter-status').value = ''; document.getElementById('filter-demand-status').value = ''; this.loadData(); }, showAddDialog() { this.editingId = null; document.getElementById('dialog-title').textContent = '新增物料需求'; document.getElementById('material-form').reset(); document.getElementById('material-dialog').style.display = 'flex'; }, editItem(id) { this.editingId = id; const item = this.data.find(d => d.id === id); if (!item) return; document.getElementById('dialog-title').textContent = '编辑物料需求'; document.getElementById('form-title').value = item.title; document.getElementById('form-list-no').value = item.listNo; document.getElementById('form-plan-no').value = item.planNo; document.getElementById('form-bom-result').value = item.bomResult; document.getElementById('form-status').value = item.status; document.getElementById('form-demand-status').value = item.demandStatus; document.getElementById('form-complete-rate').value = item.completeRate; document.getElementById('form-material-code').value = item.materialCode; document.getElementById('form-material-name').value = item.materialName; document.getElementById('form-batch-no').value = item.batchNo; document.getElementById('form-level').value = item.level; document.getElementById('form-required-qty').value = item.requiredQty; document.getElementById('form-stock-qty').value = item.stockQty; document.getElementById('form-shortage').value = item.shortage; document.getElementById('form-acquire-method').value = item.acquireMethod; document.getElementById('form-realtime-stock').value = item.realtimeStock; document.getElementById('form-pending-qty').value = item.pendingQty; document.getElementById('form-dispatched-qty').value = item.dispatchedQty; document.getElementById('form-received-qty').value = item.receivedQty; document.getElementById('material-dialog').style.display = 'flex'; }, closeDialog() { document.getElementById('material-dialog').style.display = 'none'; this.editingId = null; }, async saveData() { const form = document.getElementById('material-form'); if (!form.checkValidity()) { form.reportValidity(); return; } const data = { title: document.getElementById('form-title').value, list_no: document.getElementById('form-list-no').value, plan_no: document.getElementById('form-plan-no').value, bom_result: document.getElementById('form-bom-result').value, status: document.getElementById('form-status').value, demand_status: document.getElementById('form-demand-status').value, complete_rate: parseFloat(document.getElementById('form-complete-rate').value) || 0, material_code: document.getElementById('form-material-code').value, material_name: document.getElementById('form-material-name').value, batch_no: document.getElementById('form-batch-no').value, level: parseInt(document.getElementById('form-level').value) || 1, required_qty: parseInt(document.getElementById('form-required-qty').value) || 0, stock_qty: parseInt(document.getElementById('form-stock-qty').value) || 0, shortage: parseInt(document.getElementById('form-shortage').value) || 0, acquire_method: document.getElementById('form-acquire-method').value, realtime_stock: parseInt(document.getElementById('form-realtime-stock').value) || 0, pending_qty: parseInt(document.getElementById('form-pending-qty').value) || 0, dispatched_qty: parseInt(document.getElementById('form-dispatched-qty').value) || 0, received_qty: parseInt(document.getElementById('form-received-qty').value) || 0 }; try { const url = this.editingId ? '/api/material-purchase/update' : '/api/material-purchase/add'; if (this.editingId) { data.id = this.editingId; } const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(data) }); const result = await response.json(); if (result.ok) { API.toast(this.editingId ? '更新成功' : '添加成功', 'success'); this.closeDialog(); await this.loadData(); } else { API.toast(result.error || '保存失败', 'error'); } } catch (error) { API.toast('保存失败: ' + error.message, 'error'); } }, async deleteItem(id) { if (!confirm('确定要删除这条记录吗?\n\n删除的数据将移至回收站,可在回收站中找回。')) return; try { const response = await fetch('/api/material-purchase/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ ids: [id] }) }); const result = await response.json(); if (result.ok) { API.toast('已删除,可在回收站中找回', 'success'); await this.loadData(); } else { API.toast(result.error || '删除失败', 'error'); } } catch (error) { API.toast('删除失败: ' + error.message, 'error'); } }, exportData() { this.showExportDialog(); }, showExportDialog() { const dialog = document.getElementById('export-dialog'); if (dialog) { dialog.style.display = 'flex'; // 默认全选 document.querySelectorAll('.export-field-checkbox').forEach(cb => { cb.checked = true; }); } }, closeExportDialog() { const dialog = document.getElementById('export-dialog'); if (dialog) { dialog.style.display = 'none'; } }, async performExport() { // 获取选中的字段 const selectedFields = []; document.querySelectorAll('.export-field-checkbox:checked').forEach(cb => { selectedFields.push({ key: cb.value, label: cb.dataset.label }); }); if (selectedFields.length === 0) { API.toast('请至少选择一个导出字段', 'warning'); return; } try { // 动态加载 SheetJS 库 if (!window.XLSX) { API.toast('正在加载导出库...', 'info'); await this.loadSheetJS(); } // 准备导出数据 const exportData = this.data.map(item => { const row = {}; selectedFields.forEach(field => { let value = item[field.key]; // 格式化特殊字段 if (field.key === 'status') { value = this.getStatusText(value); } else if (field.key === 'demandStatus') { value = this.getDemandStatusText(value); } else if (field.key === 'acquireMethod') { value = this.getAcquireMethodText(value); } else if (field.key === 'submitTime' || field.key === 'updateTime') { value = this.formatDateTime(value); } else if (field.key === 'completeRate') { value = value + '%'; } row[field.label] = value || ''; }); return row; }); // 创建工作簿 const ws = window.XLSX.utils.json_to_sheet(exportData); const wb = window.XLSX.utils.book_new(); window.XLSX.utils.book_append_sheet(wb, ws, '物料清单'); // 设置列宽 const colWidths = selectedFields.map(field => { let width = 15; if (field.key === 'listNo' || field.key === 'planNo') width = 20; if (field.key === 'title' || field.key === 'materialName') width = 25; if (field.key === 'submitTime' || field.key === 'updateTime') width = 20; return { wch: width }; }); ws['!cols'] = colWidths; // 生成文件名 const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); const filename = `物料清单-采购_${timestamp}.xlsx`; // 导出文件 window.XLSX.writeFile(wb, filename); API.toast('导出成功!', 'success'); this.closeExportDialog(); } catch (error) { console.error('导出失败:', error); API.toast('导出失败: ' + error.message, 'error'); } }, async loadSheetJS() { return new Promise((resolve, reject) => { 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('加载导出库失败')); document.head.appendChild(script); }); }, selectAllFields() { document.querySelectorAll('.export-field-checkbox').forEach(cb => { cb.checked = true; }); }, deselectAllFields() { document.querySelectorAll('.export-field-checkbox').forEach(cb => { cb.checked = false; }); }, toggleRowSelection(id, checked) { if (checked) { this.selectedIds.add(id); } else { this.selectedIds.delete(id); } this.updateSelectAllCheckbox(); this.updateSelectionInfo(); }, toggleSelectAll(checkbox) { const checkboxes = document.querySelectorAll('.row-checkbox'); checkboxes.forEach(cb => { const id = parseInt(cb.value); cb.checked = checkbox.checked; if (checkbox.checked) { this.selectedIds.add(id); } else { this.selectedIds.delete(id); } }); this.updateSelectionInfo(); }, updateSelectAllCheckbox() { const selectAllCheckbox = document.getElementById('select-all-checkbox'); const checkboxes = document.querySelectorAll('.row-checkbox'); const checkedCount = document.querySelectorAll('.row-checkbox:checked').length; if (selectAllCheckbox) { selectAllCheckbox.checked = checkboxes.length > 0 && checkedCount === checkboxes.length; selectAllCheckbox.indeterminate = checkedCount > 0 && checkedCount < checkboxes.length; } }, updateSelectionInfo() { const count = this.selectedIds.size; const infoEl = document.getElementById('selection-info'); if (infoEl) { if (count > 0) { infoEl.textContent = `已选择 ${count} 项`; infoEl.style.display = 'inline-block'; } else { infoEl.style.display = 'none'; } } }, clearSelection() { this.selectedIds.clear(); document.querySelectorAll('.row-checkbox').forEach(cb => { cb.checked = false; }); const selectAllCheckbox = document.getElementById('select-all-checkbox'); if (selectAllCheckbox) { selectAllCheckbox.checked = false; selectAllCheckbox.indeterminate = false; } this.updateSelectionInfo(); }, async deleteSelected() { if (this.selectedIds.size === 0) { API.toast('请先选择要删除的项', 'warning'); return; } if (!confirm(`确定要删除选中的 ${this.selectedIds.size} 项吗?\n\n删除的数据将移至回收站,可在回收站中找回。`)) { return; } try { const ids = Array.from(this.selectedIds); const response = await fetch('/api/material-purchase/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ ids }) }); const result = await response.json(); if (result.ok) { API.toast(`已删除 ${result.count} 项,可在回收站中找回`, 'success'); this.selectedIds.clear(); await this.loadData(); this.updateSelectionInfo(); } else { API.toast(result.error || '删除失败', 'error'); } } catch (error) { API.toast('删除失败: ' + error.message, 'error'); } }, exportSelected() { if (this.selectedIds.size === 0) { API.toast('请先选择要导出的项', 'warning'); return; } // 临时保存原始数据 const originalData = this.data; // 只导出选中的数据 this.data = this.data.filter(item => this.selectedIds.has(item.id)); // 显示导出对话框 this.showExportDialog(); // 恢复原始数据 setTimeout(() => { this.data = originalData; }, 100); }, getStatusClass(status) { const map = { pending: 'warning', processing: 'info', completed: 'success' }; return map[status] || 'default'; }, getStatusText(status) { const map = { pending: '待处理', processing: '处理中', completed: '已完成' }; return map[status] || status; }, getDemandStatusClass(status) { const map = { urgent: 'danger', normal: 'info', low: 'default' }; return map[status] || 'default'; }, getDemandStatusText(status) { const map = { urgent: '紧急', normal: '正常', low: '低优先级' }; return map[status] || status; }, getAcquireMethodText(method) { if (!method) return '-'; const map = { purchase: '采购', production: '生产', transfer: '调拨', // 支持中文输入 '采购': '采购', '生产': '生产', '调拨': '调拨' }; return map[method] || method || '-'; }, formatDateTime(dateStr) { if (!dateStr) return '-'; const date = new Date(dateStr); return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); }, escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }, // 回收站相关功能 showRecycleBin() { const dialog = document.getElementById('recycle-bin-dialog'); if (dialog) { this.renderRecycleBin(); dialog.style.display = 'flex'; } }, closeRecycleBin() { const dialog = document.getElementById('recycle-bin-dialog'); if (dialog) { dialog.style.display = 'none'; } }, renderRecycleBin() { const tbody = document.getElementById('recycle-bin-tbody'); if (!tbody) return; if (this.recycleBin.length === 0) { tbody.innerHTML = `
回收站为空
`; return; } tbody.innerHTML = this.recycleBin.map(item => ` ${this.escapeHtml(item.title)} ${this.escapeHtml(item.listNo)} ${this.escapeHtml(item.materialCode)} ${this.escapeHtml(item.materialName)} ${this.formatDateTime(item.deletedAt)} `).join(''); }, async restoreItem(id) { try { const response = await fetch('/api/material-purchase/restore', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ id }) }); const result = await response.json(); if (result.ok) { API.toast('恢复成功', 'success'); await this.loadData(); this.renderRecycleBin(); } else { API.toast(result.error || '恢复失败', 'error'); } } catch (error) { API.toast('恢复失败: ' + error.message, 'error'); } }, async permanentDelete(id) { if (!confirm('确定要永久删除这条记录吗?此操作不可恢复!')) { return; } try { const response = await fetch('/api/material-purchase/permanent-delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ id }) }); const result = await response.json(); if (result.ok) { API.toast('已永久删除', 'success'); await this.loadData(); this.renderRecycleBin(); } else { API.toast(result.error || '删除失败', 'error'); } } catch (error) { API.toast('删除失败: ' + error.message, 'error'); } }, async emptyRecycleBin() { if (this.recycleBin.length === 0) { API.toast('回收站已经是空的', 'info'); return; } if (!confirm(`确定要清空回收站吗?这将永久删除 ${this.recycleBin.length} 项数据,此操作不可恢复!`)) { return; } try { const response = await fetch('/api/material-purchase/empty-recycle-bin', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include' }); const result = await response.json(); if (result.ok) { API.toast(`回收站已清空,删除了 ${result.count} 项`, 'success'); await this.loadData(); this.renderRecycleBin(); } else { API.toast(result.error || '清空失败', 'error'); } } catch (error) { API.toast('清空失败: ' + error.message, 'error'); } }, // 导入相关功能 showImportDialog() { const dialog = document.getElementById('import-dialog'); if (dialog) { // 重置状态 document.getElementById('import-file-input').value = ''; document.getElementById('import-status').style.display = 'none'; document.getElementById('import-confirm-btn').style.display = 'none'; dialog.style.display = 'flex'; } }, closeImportDialog() { const dialog = document.getElementById('import-dialog'); if (dialog) { dialog.style.display = 'none'; } }, async validateImportFile() { const fileInput = document.getElementById('import-file-input'); const file = fileInput.files[0]; if (!file) { API.toast('请选择要导入的文件', 'warning'); return; } const statusEl = document.getElementById('import-status'); const confirmBtn = document.getElementById('import-confirm-btn'); statusEl.style.display = 'block'; statusEl.style.background = 'rgba(79, 140, 255, 0.1)'; statusEl.style.color = 'var(--primary)'; statusEl.textContent = '正在验证文件...'; confirmBtn.style.display = 'none'; try { const formData = new FormData(); formData.append('file', file); const response = await fetch('/api/validate/material-purchase-file', { method: 'POST', credentials: 'include', body: formData }); const result = await response.json(); if (result.valid) { statusEl.style.background = 'rgba(34, 197, 94, 0.1)'; statusEl.style.color = '#22c55e'; statusEl.textContent = '✓ ' + result.message; confirmBtn.style.display = 'inline-block'; } else { statusEl.style.background = 'rgba(239, 68, 68, 0.1)'; statusEl.style.color = '#ef4444'; statusEl.textContent = '✗ ' + result.message; confirmBtn.style.display = 'none'; } } catch (error) { statusEl.style.background = 'rgba(239, 68, 68, 0.1)'; statusEl.style.color = '#ef4444'; statusEl.textContent = '✗ 验证失败: ' + error.message; confirmBtn.style.display = 'none'; } }, async confirmImport() { const fileInput = document.getElementById('import-file-input'); const file = fileInput.files[0]; if (!file) { API.toast('请选择要导入的文件', 'warning'); return; } const statusEl = document.getElementById('import-status'); const confirmBtn = document.getElementById('import-confirm-btn'); statusEl.style.display = 'block'; statusEl.style.background = 'rgba(79, 140, 255, 0.1)'; statusEl.style.color = 'var(--primary)'; statusEl.textContent = '正在导入数据...'; confirmBtn.disabled = true; try { const formData = new FormData(); formData.append('file', file); const response = await fetch('/api/upload/material-purchase-file', { method: 'POST', credentials: 'include', body: formData }); const result = await response.json(); if (result.ok) { statusEl.style.background = 'rgba(34, 197, 94, 0.1)'; statusEl.style.color = '#22c55e'; statusEl.textContent = '✓ ' + result.message; API.toast(`成功导入 ${result.count} 条数据`, 'success'); // 延迟关闭对话框并刷新数据 setTimeout(async () => { this.closeImportDialog(); await this.loadData(); }, 1500); } else { statusEl.style.background = 'rgba(239, 68, 68, 0.1)'; statusEl.style.color = '#ef4444'; statusEl.textContent = '✗ ' + (result.error || '导入失败'); confirmBtn.disabled = false; } } catch (error) { statusEl.style.background = 'rgba(239, 68, 68, 0.1)'; statusEl.style.color = '#ef4444'; statusEl.textContent = '✗ 导入失败: ' + error.message; confirmBtn.disabled = false; } } }; // 页面加载后初始化 Router.onAfterEach((path) => { if (path === '/plan-mgmt/material-purchase') { setTimeout(() => MaterialPurchase.init(), 100); } }); })();