import React, { useState, useEffect, useRef } from 'react';
import { Table, Card, Space, Select, DatePicker, Input, Tag, Button, message, Modal, Upload, Radio, Dropdown } from 'antd';
import { HistoryOutlined, SearchOutlined, FileTextOutlined, DownloadOutlined, UploadOutlined, FileExcelOutlined, FileOutlined, DownOutlined } from '@ant-design/icons';
import axios from 'axios';
import dayjs from 'dayjs';
import * as XLSX from 'xlsx';
const { RangePicker } = DatePicker;
const { Option } = Select;
function ConsumableLogs() {
const [logs, setLogs] = useState([]);
const [loading, setLoading] = useState(true);
const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
const [filters, setFilters] = useState({
operationType: 'all',
consumableId: '',
dateRange: null
});
const [importModalVisible, setImportModalVisible] = useState(false);
const [importType, setImportType] = useState('excel');
const [importing, setImporting] = useState(false);
const fileInputRef = useRef(null);
const fetchLogs = async (page = 1, pageSize = 10, currentFilters = filters) => {
try {
setLoading(true);
const params = { page, pageSize };
if (currentFilters.operationType !== 'all') {
params.operationType = currentFilters.operationType;
}
if (currentFilters.consumableId) {
params.consumableId = currentFilters.consumableId;
}
if (currentFilters.dateRange) {
params.startDate = currentFilters.dateRange[0].format('YYYY-MM-DD');
params.endDate = currentFilters.dateRange[1].format('YYYY-MM-DD');
}
const response = await axios.get('/api/consumables/logs', { params });
setLogs(response.data.logs);
setPagination(prev => ({ ...prev, current: page, total: response.data.total }));
} catch (error) {
message.error('获取操作日志失败');
console.error('获取操作日志失败:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchLogs(1, pagination.pageSize, filters);
}, [filters.operationType, filters.consumableId, filters.dateRange]);
const handleFilterChange = (key, value) => {
setFilters(prev => ({ ...prev, [key]: value }));
fetchLogs(1, pagination.pageSize);
};
const getOperationTag = (type) => {
const config = {
in: { color: 'green', text: '入库' },
out: { color: 'red', text: '出库' },
create: { color: 'blue', text: '创建' },
update: { color: 'orange', text: '更新' },
delete: { color: 'magenta', text: '删除' },
adjust: { color: 'purple', text: '调整' },
import: { color: 'cyan', text: '导入' }
};
const { color, text } = config[type] || { color: 'default', text: type };
return {value}
},
{
title: '耗材名称',
dataIndex: 'consumableName',
key: 'consumableName',
width: 150
},
{
title: '操作类型',
dataIndex: 'operationType',
key: 'operationType',
width: 100,
render: (type) => getOperationTag(type)
},
{
title: '变动数量',
dataIndex: 'quantity',
key: 'quantity',
width: 100,
render: (value, record) => (
0 ? '#52c41a' : value < 0 ? '#ff4d4f' : '#888',
fontWeight: 'bold'
}}>
{value > 0 ? '+' : ''}{value}
)
},
{
title: '操作前库存',
dataIndex: 'previousStock',
key: 'previousStock',
width: 100
},
{
title: '操作后库存',
dataIndex: 'currentStock',
key: 'currentStock',
width: 100
},
{
title: '操作人',
dataIndex: 'operator',
key: 'operator',
width: 120
},
{
title: '原因',
dataIndex: 'reason',
key: 'reason',
width: 150,
render: (value) => value || '-'
},
{
title: '备注',
dataIndex: 'notes',
key: 'notes',
width: 200,
render: (value) => value || '-',
ellipsis: true
}
];
const handleExport = async (currentFilters = filters) => {
try {
const params = {};
if (currentFilters.operationType !== 'all') {
params.operationType = currentFilters.operationType;
}
if (currentFilters.consumableId) {
params.consumableId = currentFilters.consumableId;
}
if (currentFilters.dateRange) {
params.startDate = currentFilters.dateRange[0].format('YYYY-MM-DD');
params.endDate = currentFilters.dateRange[1].format('YYYY-MM-DD');
}
const response = await axios.get('/api/consumables/logs/export', {
params,
responseType: 'blob'
});
const blob = new Blob([response.data], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = `耗材操作日志_${dayjs().format('YYYYMMDD_HHmmss')}.csv`;
link.click();
message.success('导出成功');
} catch (error) {
message.error('导出失败');
console.error('导出失败:', error);
}
};
const handleExportExcel = async (currentFilters = filters) => {
try {
const params = {};
if (currentFilters.operationType !== 'all') {
params.operationType = currentFilters.operationType;
}
if (currentFilters.consumableId) {
params.consumableId = currentFilters.consumableId;
}
if (currentFilters.dateRange) {
params.startDate = currentFilters.dateRange[0].format('YYYY-MM-DD');
params.endDate = currentFilters.dateRange[1].format('YYYY-MM-DD');
}
const response = await axios.get('/api/consumables/logs', {
params: { ...params, page: 1, pageSize: 10000 }
});
const exportData = response.data.logs.map(log => ({
'时间': dayjs(log.createdAt).format('YYYY-MM-DD HH:mm:ss'),
'耗材ID': log.consumableId,
'耗材名称': log.consumableName,
'操作类型': getOperationTypeText(log.operationType),
'变动数量': log.quantity,
'操作前库存': log.previousStock,
'操作后库存': log.currentStock,
'操作人': log.operator,
'原因': log.reason || '',
'备注': log.notes || ''
}));
const ws = XLSX.utils.json_to_sheet(exportData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, '操作日志');
XLSX.writeFile(wb, `耗材操作日志_${dayjs().format('YYYYMMDD_HHmmss')}.xlsx`);
message.success('导出Excel成功');
} catch (error) {
message.error('导出Excel失败');
console.error('导出Excel失败:', error);
}
};
const getOperationTypeText = (type) => {
const map = {
'in': '入库',
'out': '出库',
'create': '创建',
'update': '更新',
'delete': '删除',
'adjust': '调整',
'import': '导入'
};
return map[type] || type;
};
const handleImport = async (file) => {
setImporting(true);
try {
const reader = new FileReader();
reader.onload = async (e) => {
try {
let logItems = [];
if (importType === 'excel') {
const workbook = XLSX.read(e.target.result, { type: 'array' });
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
logItems = XLSX.utils.sheet_to_json(worksheet);
} else {
const text = e.target.result;
const lines = text.split('\n').filter(line => line.trim());
const headers = lines[0].split(',').map(h => h.replace(/"/g, ''));
for (let i = 1; i < lines.length; i++) {
const values = lines[i].split(',').map(v => v.replace(/"/g, ''));
const item = {};
headers.forEach((h, idx) => {
item[h] = values[idx];
});
logItems.push(item);
}
}
const response = await axios.post('/api/consumables/logs/import', {
logs: logItems,
operator: '前端导入'
});
if (response.data.success > 0) {
message.success(`成功导入 ${response.data.success} 条记录`);
}
if (response.data.failed > 0) {
message.warning(`导入失败 ${response.data.failed} 条`);
response.data.errors.forEach(err => console.error(err));
}
setImportModalVisible(false);
fetchLogs(1, pagination.pageSize);
} catch (err) {
message.error('解析文件失败: ' + err.message);
} finally {
setImporting(false);
}
};
if (importType === 'excel') {
reader.readAsArrayBuffer(file);
} else {
reader.readAsText(file);
}
} catch (error) {
message.error('导入失败');
setImporting(false);
}
return false;
};
const downloadTemplate = () => {
const template = [
{
'耗材ID': 'CON123456',
'耗材名称': '示例耗材',
'操作类型': '入库',
'变动数量': 10,
'操作前库存': 100,
'操作后库存': 110,
'操作人': '管理员',
'原因': '示例原因',
'备注': '示例备注'
}
];
const ws = XLSX.utils.json_to_sheet(template);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, '日志模板');
XLSX.writeFile(wb, '耗材操作日志导入模板.xlsx');
message.success('模板下载成功');
};
return (