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 {text}; }; const columns = [ { title: '时间', dataIndex: 'createdAt', key: 'createdAt', width: 180, sorter: (a, b) => new Date(a.createdAt) - new Date(b.createdAt), render: (date) => dayjs(date).format('YYYY-MM-DD HH:mm:ss') }, { title: '耗材ID', dataIndex: 'consumableId', key: 'consumableId', width: 150, render: (value) => {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 (
耗材操作日志 } > handleFilterChange('consumableId', value)} prefix={} /> handleFilterChange('dateRange', dates)} placeholder={['开始日期', '结束日期']} /> , label: '导出CSV', onClick: () => handleExport(filters) }, { key: 'excel', icon: , label: '导出Excel', onClick: () => handleExportExcel(filters) } ] }} > `共 ${total} 条记录`, showSizeChanger: true, showQuickJumper: true }} onChange={(pagination) => fetchLogs(pagination.current, pagination.pageSize)} scroll={{ x: 1500 }} /> { setImportModalVisible(false); setImportType('excel'); }} footer={null} width={500} >
setImportType(e.target.value)} style={{ marginBottom: 16 }} > Excel文件 CSV文件
(建议先下载模板填写)

注意事项:

  • 支持 Excel (.xlsx, .xls) 或 CSV 格式
  • 文件列名必须包含:耗材ID、操作类型
  • 操作类型支持:入库、出库、创建、更新、删除、调整、导入
  • 系统将根据筛选条件过滤需要导出的数据
); } export default ConsumableLogs;