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) => (