import React, { useState, useEffect, useRef } from 'react'; import { Table, Button, Modal, Form, Input, Select, InputNumber, message, Card, Space, Popconfirm, Upload, Table as AntTable } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ExportOutlined, ImportOutlined, UploadOutlined, FileExcelOutlined, InboxOutlined } from '@ant-design/icons'; import axios from 'axios'; const { Option } = Select; function ConsumableManagement() { const [consumables, setConsumables] = useState([]); const [loading, setLoading] = useState(true); const [modalVisible, setModalVisible] = useState(false); const [editingConsumable, setEditingConsumable] = useState(null); const [form] = Form.useForm(); const [categories, setCategories] = useState([]); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, showTotal: (total) => `共 ${total} 条记录` }); const [keyword, setKeyword] = useState(''); const [category, setCategory] = useState('all'); const [status, setStatus] = useState('all'); const [importModalVisible, setImportModalVisible] = useState(false); const [importPreview, setImportPreview] = useState([]); const [importFile, setImportFile] = useState(null); const [importing, setImporting] = useState(false); const importFormRef = useRef(null); const [stockModalVisible, setStockModalVisible] = useState(false); const [stockRecord, setStockRecord] = useState(null); const [stockType, setStockType] = useState('in'); const [stockForm] = Form.useForm(); const fetchConsumables = async (page = 1, pageSize = 10) => { try { setLoading(true); const response = await axios.get('/api/consumables', { params: { page, pageSize, keyword, category, status } }); setConsumables(response.data.consumables); setPagination(prev => ({ ...prev, current: page, pageSize, total: response.data.total })); } catch (error) { message.error('获取耗材列表失败'); console.error('获取耗材列表失败:', error); } finally { setLoading(false); } }; const fetchCategories = async () => { try { const response = await axios.get('/api/consumable-categories/list'); setCategories(response.data); } catch (error) { console.error('获取分类列表失败:', error); } }; useEffect(() => { fetchConsumables(); fetchCategories(); }, [keyword, category, status]); const showModal = (consumable = null) => { setEditingConsumable(consumable); if (consumable) { form.setFieldsValue(consumable); } else { form.resetFields(); } setModalVisible(true); }; const handleCancel = () => { setModalVisible(false); setEditingConsumable(null); }; const handleSubmit = async (values) => { try { if (editingConsumable) { await axios.put(`/api/consumables/${editingConsumable.consumableId}`, values); message.success('耗材更新成功'); } else { await axios.post('/api/consumables', { ...values, consumableId: `CON${Date.now()}` }); message.success('耗材创建成功'); } setModalVisible(false); fetchConsumables(); setEditingConsumable(null); } catch (error) { message.error(editingConsumable ? '耗材更新失败' : '耗材创建失败'); console.error('提交失败:', error); } }; const handleDelete = async (consumableId) => { try { await axios.delete(`/api/consumables/${consumableId}`); message.success('删除成功'); fetchConsumables(); } catch (error) { message.error('删除失败'); console.error('删除失败:', error); } }; const handleSearch = (value) => { setKeyword(value); }; const exportToCSV = (data, filename) => { const headers = ['耗材ID', '名称', '分类', '单位', '当前库存', '最小库存', '最大库存', '单价', '供应商', '存放位置', '状态']; const keys = ['consumableId', 'name', 'category', 'unit', 'currentStock', 'minStock', 'maxStock', 'unitPrice', 'supplier', 'location', 'status']; const csvContent = [ headers.join(','), ...data.map(row => keys.map(key => { let value = row[key]; if (key === 'unitPrice') value = `¥${parseFloat(value || 0).toFixed(2)}`; if (key === 'status') value = value === 'active' ? '启用' : '停用'; if (value === null || value === undefined) value = ''; const str = String(value); if (str.includes(',') || str.includes('"') || str.includes('\n')) { return `"${str.replace(/"/g, '""')}"`; } return str; }).join(',')) ].join('\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); }; const handleExport = async () => { try { const response = await axios.get('/api/consumables', { params: { keyword, category, status, pageSize: 1000 } }); const consumables = response.data.consumables; exportToCSV(consumables, `consumables_${new Date().toISOString().split('T')[0]}.csv`); message.success('导出成功'); } catch (error) { message.error('导出失败'); console.error('导出失败:', error); } }; const parseCSV = (text) => { const lines = text.trim().split('\n'); if (lines.length < 2) return []; const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, '')); const data = []; for (let i = 1; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; let values = []; let inQuotes = false; let current = ''; for (let j = 0; j < line.length; j++) { const char = line[j]; if (char === '"') { inQuotes = !inQuotes; } else if (char === ',' && !inQuotes) { values.push(current.trim().replace(/^"|"$/g, '')); current = ''; } else { current += char; } } values.push(current.trim().replace(/^"|"$/g, '')); const row = {}; headers.forEach((header, idx) => { row[header] = values[idx] || ''; }); data.push(row); } return data; }; const handleFileChange = (info) => { const file = info.fileList[info.fileList.length - 1]; if (file && file.originFileObj) { const reader = new FileReader(); reader.onload = (e) => { const text = e.target.result; const parsedData = parseCSV(text); setImportPreview(parsedData.slice(0, 10)); setImportFile(file.originFileObj); }; reader.readAsText(file.originFileObj); } }; const showImportModal = () => { setImportPreview([]); setImportFile(null); setImportModalVisible(true); }; const handleImportCancel = () => { setImportModalVisible(false); setImportPreview([]); setImportFile(null); }; const handleImport = async () => { if (!importFile) { message.warning('请先选择文件'); return; } setImporting(true); try { const reader = new FileReader(); reader.onload = async (e) => { const text = e.target.result; const items = parseCSV(text); const response = await axios.post('/api/consumables/import', { items }); message.success(response.data.message); if (response.data.results.failed > 0) { response.data.results.errors.forEach(err => console.error(err)); } setImportModalVisible(false); setImportPreview([]); setImportFile(null); fetchConsumables(); setImporting(false); }; reader.readAsText(importFile); } catch (error) { message.error('导入失败'); console.error('导入失败:', error); setImporting(false); } }; const downloadTemplate = () => { const template = '耗材ID,名称,分类,单位,当前库存,最小库存,最大库存,单价,供应商,存放位置,描述,状态\n,测试耗材,办公用品,个,100,10,500,5.00,XX公司,A柜-01层,测试数据,active'; const blob = new Blob([template], { type: 'text/csv;charset=utf-8;' }); const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = '耗材导入模板.csv'; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); }; const showStockModal = (record, type) => { setStockRecord(record); setStockType(type); stockForm.setFieldsValue({ consumableId: record.consumableId, consumableName: record.name, quantity: 1, reason: '', notes: '' }); setStockModalVisible(true); }; const handleStockCancel = () => { setStockModalVisible(false); setStockRecord(null); }; const handleStockSubmit = async (values) => { try { const response = await axios.post('/api/consumables/quick-inout', { consumableId: stockRecord.consumableId, type: stockType, quantity: values.quantity, operator: values.operator || '系统管理员', reason: values.reason, notes: values.notes }); message.success(`${stockType === 'in' ? '入库' : '出库'}操作成功`); setStockModalVisible(false); fetchConsumables(); } catch (error) { message.error(error.response?.data?.error || `${stockType === 'in' ? '入库' : '出库'}操作失败`); console.error('操作失败:', error); } }; const columns = [ { title: '耗材ID', dataIndex: 'consumableId', key: 'consumableId', width: 150 }, { title: '名称', dataIndex: 'name', key: 'name', width: 150 }, { title: '分类', dataIndex: 'category', key: 'category', width: 120 }, { title: '单位', dataIndex: 'unit', key: 'unit', width: 80 }, { title: '当前库存', dataIndex: 'currentStock', key: 'currentStock', width: 100, render: (value, record) => { const isLow = value <= record.minStock; return ( {value} ); } }, { title: '最小库存', dataIndex: 'minStock', key: 'minStock', width: 100 }, { title: '最大库存', dataIndex: 'maxStock', key: 'maxStock', width: 100 }, { title: '单价(元)', dataIndex: 'unitPrice', key: 'unitPrice', width: 100, render: (value) => `¥${parseFloat(value || 0).toFixed(2)}` }, { title: '供应商', dataIndex: 'supplier', key: 'supplier', width: 150, render: (value) => value || '-' }, { title: '位置', dataIndex: 'location', key: 'location', width: 120, render: (value) => value || '-' }, { title: '状态', dataIndex: 'status', key: 'status', width: 100, render: (value) => ( {value === 'active' ? '启用' : '停用'} ) }, { title: '操作', key: 'action', width: 200, render: (_, record) => ( handleDelete(record.consumableId)}> ) } ]; const previewColumns = [ { title: '名称', dataIndex: '名称', key: 'name', width: 120 }, { title: '分类', dataIndex: '分类', key: 'category', width: 100 }, { title: '单位', dataIndex: '单位', key: 'unit', width: 80 }, { title: '当前库存', dataIndex: '当前库存', key: 'currentStock', width: 90 }, { title: '单价', dataIndex: '单价', key: 'unitPrice', width: 80 } ]; return (
}> fetchConsumables(pagination.current, pagination.pageSize)} scroll={{ x: 1300 }} />
请下载模板后填写数据再导入 false} onChange={handleFileChange} > {importPreview.length > 0 && (
预览 (前10条):
index} pagination={false} size="small" scroll={{ x: 500 }} /> )}
); } export default ConsumableManagement;