import React, { useState, useEffect, useRef } from 'react'; import { useDebounce } from '../hooks/useDebounce'; import { Table, Card, Space, Select, DatePicker, Input, Tag, Button, message, Modal, Upload, Radio, Dropdown, Form, Tooltip, Timeline, Row, Col, Statistic, Divider, Popover, } from 'antd'; import { HistoryOutlined, SearchOutlined, FileTextOutlined, DownloadOutlined, UploadOutlined, FileExcelOutlined, FileOutlined, DownOutlined, EditOutlined, EyeOutlined, } from '@ant-design/icons'; import axios from 'axios'; import CloseButton from '../components/CloseButton'; import dayjs from 'dayjs'; import * as XLSX from 'xlsx'; import { inputStyles, selectStyles, textAreaStyles, filterInputStyles, datePickerStyles, inputPlaceholders, inputValidationRules, } from '../styles/deviceManagementStyles'; 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: ['in', 'out'], consumableId: '', dateRange: null, }); const debouncedConsumableId = useDebounce(filters.consumableId, 300); const [importModalVisible, setImportModalVisible] = useState(false); const [importType, setImportType] = useState('excel'); const [importing, setImporting] = useState(false); const [editModalVisible, setEditModalVisible] = useState(false); const [historyModalVisible, setHistoryModalVisible] = useState(false); const [currentLog, setCurrentLog] = useState(null); const [logHistory, setLogHistory] = useState([]); const [editLoading, setEditLoading] = useState(false); const [historyLoading, setHistoryLoading] = useState(false); const [form] = Form.useForm(); const fileInputRef = useRef(null); // 归档详情弹窗状态 const [archiveModalVisible, setArchiveModalVisible] = useState(false); const [currentArchive, setCurrentArchive] = useState(null); const [archiveLoading, setArchiveLoading] = useState(false); const fetchLogs = async (page = 1, pageSize = 10, currentFilters = filters) => { try { setLoading(true); const params = { page, pageSize }; if (currentFilters.operationType && currentFilters.operationType !== 'all' && currentFilters.operationType.length > 0) { params.operationType = currentFilters.operationType.join(','); } 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, consumableId: debouncedConsumableId, }); }, [debouncedConsumableId, filters.operationType, filters.dateRange]); const handleFilterChange = (key, value) => { setFilters(prev => ({ ...prev, [key]: value })); }; 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: 180, render: (value, record) => (
分类: {record.consumableSnapshot.category || '-'}
单位: {record.consumableSnapshot.unit || '-'}
单价: {record.consumableSnapshot.unitPrice || '-'}
供应商: {record.consumableSnapshot.supplier || '-'}
位置: {record.consumableSnapshot.location || '-'}
) : null } > {value} {record.isConsumableDeleted && ( 已删除 )}
), }, { title: '操作类型', dataIndex: 'operationType', key: 'operationType', width: 120, render: (type, record) => ( {getOperationTag(type)} {type === 'delete' && record.relatedId && ( handleViewArchive(record.relatedId)} > 已归档 )} ), }, { 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: 'SN序列号', dataIndex: 'snList', key: 'snList', width: 150, render: (snList, record) => { const snListArray = Array.isArray(snList) ? snList : []; if (!snListArray || snListArray.length === 0) { return '-'; } if (record.operationType !== 'in' && record.operationType !== 'out') { return '-'; } if (snListArray.length <= 3) { return ( {snListArray.map((sn, index) => ( {sn} ))} ); } const content = (
{snListArray.map((sn, index) => ( {sn} ))}
); return ( {snListArray.length} 个SN 🔍 ); }, }, { title: '备注', dataIndex: 'notes', key: 'notes', width: 200, render: value => ( {value || '-'} ), }, { title: '操作', key: 'action', width: 120, fixed: 'right', render: (_, record) => ( {record.isEditable && ( , 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 }} /> } onCancel={() => { setImportModalVisible(false); setImportType('excel'); }} footer={null} width={500} >
setImportType(e.target.value)} style={{ marginBottom: 16 }} > Excel文件 CSV文件
(建议先下载模板填写)

注意事项:

{/* 编辑日志弹窗 */} } onCancel={() => { setEditModalVisible(false); form.resetFields(); }} onOk={() => form.submit()} confirmLoading={editLoading} width={600} >
{/* 修改历史弹窗 */} } onCancel={() => { setHistoryModalVisible(false); setLogHistory([]); }} footer={null} width={700} > {historyLoading ? (
加载中...
) : logHistory.length <= 1 ? (
该记录暂无修改历史
) : ( {logHistory.map((item, index) => (
{getOperationTag(item.operationType).props.children} {item.modifiedBy && 已修改}

耗材: {item.consumableName} ({item.consumableId}) {item.isConsumableDeleted && 已删除}

操作人: {item.operator}

{item.reason && (

原因: {item.reason}

)} {item.notes && (

备注: {item.notes}

)} {item.consumableSnapshot && (

快照信息: 分类:{item.consumableSnapshot.category || '-'} | 单位:{item.consumableSnapshot.unit || '-'} | 单价:{item.consumableSnapshot.unitPrice || '-'}

)} {item.modifiedBy && ( <>

修改人: {item.modifiedBy}

修改时间:{' '} {dayjs(item.modifiedAt).format('YYYY-MM-DD HH:mm:ss')}

{item.modificationReason && (

修改原因: {item.modificationReason}

)} )}
))}
)}
{/* 归档详情弹窗 */} } onCancel={() => { setArchiveModalVisible(false); setCurrentArchive(null); }} footer={null} width={600} > {archiveLoading ? (
加载中...
) : !currentArchive ? (
无法获取归档信息
) : (

归档ID: {currentArchive.archiveId}

耗材ID: {currentArchive.consumableId}

耗材名称: {currentArchive.consumableName}

删除人: {currentArchive.deletedBy}

删除时间: {dayjs(currentArchive.deletedAt).format('YYYY-MM-DD HH:mm:ss')}

删除原因: {currentArchive.deleteReason || '-'}

首次操作: {currentArchive.firstOperationAt ? dayjs(currentArchive.firstOperationAt).format('YYYY-MM-DD HH:mm:ss') : '-'}

最后操作: {currentArchive.lastOperationAt ? dayjs(currentArchive.lastOperationAt).format('YYYY-MM-DD HH:mm:ss') : '-'}

删除时库存: {currentArchive.finalStock}

{currentArchive.consumableSnapshot && (

分类: {currentArchive.consumableSnapshot.category || '-'}

单位: {currentArchive.consumableSnapshot.unit || '-'}

单价: {currentArchive.consumableSnapshot.unitPrice || '-'}

供应商: {currentArchive.consumableSnapshot.supplier || '-'}

位置: {currentArchive.consumableSnapshot.location || '-'}

最小库存: {currentArchive.consumableSnapshot.minStock || '-'}

最大库存: {currentArchive.consumableSnapshot.maxStock || '-'}

)} )} ); } export default ConsumableLogs;