2025-12-24 16:44:27 +08:00
|
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
2026-02-26 11:24:00 +08:00
|
|
|
|
import { useDebounce } from '../hooks/useDebounce';
|
2026-02-10 11:04:01 +08:00
|
|
|
|
import {
|
|
|
|
|
|
Table,
|
|
|
|
|
|
Card,
|
|
|
|
|
|
Space,
|
|
|
|
|
|
Select,
|
|
|
|
|
|
DatePicker,
|
|
|
|
|
|
Input,
|
|
|
|
|
|
Tag,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
message,
|
|
|
|
|
|
Modal,
|
|
|
|
|
|
Upload,
|
|
|
|
|
|
Radio,
|
|
|
|
|
|
Dropdown,
|
|
|
|
|
|
Form,
|
|
|
|
|
|
Tooltip,
|
|
|
|
|
|
Timeline,
|
2026-02-24 13:20:26 +08:00
|
|
|
|
Row,
|
|
|
|
|
|
Col,
|
|
|
|
|
|
Statistic,
|
|
|
|
|
|
Divider,
|
2026-03-05 10:22:44 +08:00
|
|
|
|
Popover,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
} from 'antd';
|
|
|
|
|
|
import {
|
|
|
|
|
|
HistoryOutlined,
|
|
|
|
|
|
SearchOutlined,
|
|
|
|
|
|
FileTextOutlined,
|
|
|
|
|
|
DownloadOutlined,
|
|
|
|
|
|
UploadOutlined,
|
|
|
|
|
|
FileExcelOutlined,
|
|
|
|
|
|
FileOutlined,
|
|
|
|
|
|
DownOutlined,
|
|
|
|
|
|
EditOutlined,
|
|
|
|
|
|
EyeOutlined,
|
|
|
|
|
|
} from '@ant-design/icons';
|
2025-12-24 16:44:27 +08:00
|
|
|
|
import axios from 'axios';
|
2026-03-12 12:40:12 +08:00
|
|
|
|
import CloseButton from '../components/CloseButton';
|
2025-12-24 16:44:27 +08:00
|
|
|
|
import dayjs from 'dayjs';
|
|
|
|
|
|
import * as XLSX from 'xlsx';
|
2026-03-05 10:41:49 +08:00
|
|
|
|
import {
|
|
|
|
|
|
inputStyles,
|
|
|
|
|
|
selectStyles,
|
|
|
|
|
|
textAreaStyles,
|
|
|
|
|
|
filterInputStyles,
|
|
|
|
|
|
datePickerStyles,
|
|
|
|
|
|
inputPlaceholders,
|
|
|
|
|
|
inputValidationRules,
|
|
|
|
|
|
} from '../styles/deviceManagementStyles';
|
2025-12-24 16:44:27 +08:00
|
|
|
|
|
|
|
|
|
|
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({
|
2026-03-05 09:57:44 +08:00
|
|
|
|
operationType: ['in', 'out'],
|
2025-12-24 16:44:27 +08:00
|
|
|
|
consumableId: '',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
dateRange: null,
|
2025-12-24 16:44:27 +08:00
|
|
|
|
});
|
2026-02-26 11:24:00 +08:00
|
|
|
|
const debouncedConsumableId = useDebounce(filters.consumableId, 300);
|
2025-12-24 16:44:27 +08:00
|
|
|
|
const [importModalVisible, setImportModalVisible] = useState(false);
|
|
|
|
|
|
const [importType, setImportType] = useState('excel');
|
|
|
|
|
|
const [importing, setImporting] = useState(false);
|
2026-01-30 14:01:23 +08:00
|
|
|
|
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();
|
2025-12-24 16:44:27 +08:00
|
|
|
|
const fileInputRef = useRef(null);
|
|
|
|
|
|
|
2026-02-24 13:20:26 +08:00
|
|
|
|
// 归档详情弹窗状态
|
|
|
|
|
|
const [archiveModalVisible, setArchiveModalVisible] = useState(false);
|
|
|
|
|
|
const [currentArchive, setCurrentArchive] = useState(null);
|
|
|
|
|
|
const [archiveLoading, setArchiveLoading] = useState(false);
|
|
|
|
|
|
|
2025-12-24 16:44:27 +08:00
|
|
|
|
const fetchLogs = async (page = 1, pageSize = 10, currentFilters = filters) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
const params = { page, pageSize };
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-03-05 09:57:44 +08:00
|
|
|
|
if (currentFilters.operationType && currentFilters.operationType !== 'all' && currentFilters.operationType.length > 0) {
|
|
|
|
|
|
params.operationType = currentFilters.operationType.join(',');
|
2025-12-24 16:44:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
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');
|
|
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2025-12-24 16:44:27 +08:00
|
|
|
|
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(() => {
|
2026-02-26 11:24:00 +08:00
|
|
|
|
fetchLogs(1, pagination.pageSize, {
|
|
|
|
|
|
...filters,
|
|
|
|
|
|
consumableId: debouncedConsumableId,
|
|
|
|
|
|
});
|
|
|
|
|
|
}, [debouncedConsumableId, filters.operationType, filters.dateRange]);
|
2025-12-24 16:44:27 +08:00
|
|
|
|
|
|
|
|
|
|
const handleFilterChange = (key, value) => {
|
|
|
|
|
|
setFilters(prev => ({ ...prev, [key]: value }));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const getOperationTag = type => {
|
2025-12-24 16:44:27 +08:00
|
|
|
|
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: '调整' },
|
2026-02-10 11:04:01 +08:00
|
|
|
|
import: { color: 'cyan', text: '导入' },
|
2025-12-24 16:44:27 +08:00
|
|
|
|
};
|
|
|
|
|
|
const { color, text } = config[type] || { color: 'default', text: type };
|
|
|
|
|
|
return <Tag color={color}>{text}</Tag>;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const columns = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '时间',
|
|
|
|
|
|
dataIndex: 'createdAt',
|
|
|
|
|
|
key: 'createdAt',
|
|
|
|
|
|
width: 180,
|
|
|
|
|
|
sorter: (a, b) => new Date(a.createdAt) - new Date(b.createdAt),
|
2026-02-10 11:04:01 +08:00
|
|
|
|
render: date => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
|
2025-12-24 16:44:27 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '耗材ID',
|
|
|
|
|
|
dataIndex: 'consumableId',
|
|
|
|
|
|
key: 'consumableId',
|
|
|
|
|
|
width: 150,
|
2026-02-24 13:20:26 +08:00
|
|
|
|
render: value => <code>{value}</code>,
|
2025-12-24 16:44:27 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '耗材名称',
|
|
|
|
|
|
dataIndex: 'consumableName',
|
|
|
|
|
|
key: 'consumableName',
|
2026-02-24 13:20:26 +08:00
|
|
|
|
width: 180,
|
2026-02-24 11:58:31 +08:00
|
|
|
|
render: (value, record) => (
|
|
|
|
|
|
<Tooltip
|
|
|
|
|
|
title={
|
|
|
|
|
|
record.consumableSnapshot ? (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div>分类: {record.consumableSnapshot.category || '-'}</div>
|
|
|
|
|
|
<div>单位: {record.consumableSnapshot.unit || '-'}</div>
|
|
|
|
|
|
<div>单价: {record.consumableSnapshot.unitPrice || '-'}</div>
|
|
|
|
|
|
<div>供应商: {record.consumableSnapshot.supplier || '-'}</div>
|
|
|
|
|
|
<div>位置: {record.consumableSnapshot.location || '-'}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
2026-02-24 13:20:26 +08:00
|
|
|
|
<Space direction="vertical" size={0}>
|
|
|
|
|
|
<span>{value}</span>
|
|
|
|
|
|
{record.isConsumableDeleted && (
|
|
|
|
|
|
<Tag color="red" size="small">已删除</Tag>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Space>
|
2026-02-24 11:58:31 +08:00
|
|
|
|
</Tooltip>
|
|
|
|
|
|
),
|
2025-12-24 16:44:27 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '操作类型',
|
|
|
|
|
|
dataIndex: 'operationType',
|
|
|
|
|
|
key: 'operationType',
|
2026-02-24 13:20:26 +08:00
|
|
|
|
width: 120,
|
|
|
|
|
|
render: (type, record) => (
|
|
|
|
|
|
<Space direction="vertical" size={0}>
|
|
|
|
|
|
{getOperationTag(type)}
|
|
|
|
|
|
{type === 'delete' && record.relatedId && (
|
|
|
|
|
|
<Tooltip title="点击查看归档详情">
|
|
|
|
|
|
<Tag
|
|
|
|
|
|
color="blue"
|
|
|
|
|
|
style={{ cursor: 'pointer', fontSize: '11px' }}
|
|
|
|
|
|
onClick={() => handleViewArchive(record.relatedId)}
|
|
|
|
|
|
>
|
|
|
|
|
|
已归档
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
),
|
2025-12-24 16:44:27 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '变动数量',
|
|
|
|
|
|
dataIndex: 'quantity',
|
|
|
|
|
|
key: 'quantity',
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
render: (value, record) => (
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<span
|
|
|
|
|
|
style={{
|
|
|
|
|
|
color: value > 0 ? '#52c41a' : value < 0 ? '#ff4d4f' : '#888',
|
|
|
|
|
|
fontWeight: 'bold',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{value > 0 ? '+' : ''}
|
|
|
|
|
|
{value}
|
2025-12-24 16:44:27 +08:00
|
|
|
|
</span>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
),
|
2025-12-24 16:44:27 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '操作前库存',
|
|
|
|
|
|
dataIndex: 'previousStock',
|
|
|
|
|
|
key: 'previousStock',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
width: 100,
|
2025-12-24 16:44:27 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '操作后库存',
|
|
|
|
|
|
dataIndex: 'currentStock',
|
|
|
|
|
|
key: 'currentStock',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
width: 100,
|
2025-12-24 16:44:27 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '操作人',
|
|
|
|
|
|
dataIndex: 'operator',
|
|
|
|
|
|
key: 'operator',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
width: 120,
|
2025-12-24 16:44:27 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '原因',
|
|
|
|
|
|
dataIndex: 'reason',
|
|
|
|
|
|
key: 'reason',
|
|
|
|
|
|
width: 150,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
render: value => value || '-',
|
2025-12-24 16:44:27 +08:00
|
|
|
|
},
|
2026-03-05 10:22:44 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: 'SN序列号',
|
|
|
|
|
|
dataIndex: 'snList',
|
|
|
|
|
|
key: 'snList',
|
|
|
|
|
|
width: 150,
|
|
|
|
|
|
render: (snList, record) => {
|
2026-03-13 15:13:58 +08:00
|
|
|
|
const snListArray = Array.isArray(snList) ? snList : [];
|
|
|
|
|
|
if (!snListArray || snListArray.length === 0) {
|
2026-03-05 10:22:44 +08:00
|
|
|
|
return '-';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (record.operationType !== 'in' && record.operationType !== 'out') {
|
|
|
|
|
|
return '-';
|
|
|
|
|
|
}
|
2026-03-13 15:13:58 +08:00
|
|
|
|
if (snListArray.length <= 3) {
|
2026-03-05 10:22:44 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<Space size={2} wrap>
|
2026-03-13 15:13:58 +08:00
|
|
|
|
{snListArray.map((sn, index) => (
|
2026-03-05 10:22:44 +08:00
|
|
|
|
<Tag key={index} color="purple" style={{ fontSize: '11px' }}>
|
|
|
|
|
|
{sn}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
const content = (
|
|
|
|
|
|
<div style={{ maxHeight: '200px', overflowY: 'auto', maxWidth: '300px' }}>
|
2026-03-13 15:13:58 +08:00
|
|
|
|
{snListArray.map((sn, index) => (
|
2026-03-05 10:22:44 +08:00
|
|
|
|
<Tag key={index} color="purple" style={{ marginBottom: '4px', fontSize: '11px' }}>
|
|
|
|
|
|
{sn}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
return (
|
2026-03-13 15:13:58 +08:00
|
|
|
|
<Popover content={content} title={`SN列表 (${snListArray.length}个)`} trigger="click">
|
2026-03-05 10:22:44 +08:00
|
|
|
|
<Tag color="purple" style={{ cursor: 'pointer' }}>
|
2026-03-13 15:13:58 +08:00
|
|
|
|
{snListArray.length} 个SN 🔍
|
2026-03-05 10:22:44 +08:00
|
|
|
|
</Tag>
|
|
|
|
|
|
</Popover>
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-12-24 16:44:27 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: '备注',
|
|
|
|
|
|
dataIndex: 'notes',
|
|
|
|
|
|
key: 'notes',
|
|
|
|
|
|
width: 200,
|
2026-02-24 13:20:26 +08:00
|
|
|
|
render: value => (
|
|
|
|
|
|
<Tooltip title={value || '-'}>
|
|
|
|
|
|
<span style={{
|
|
|
|
|
|
display: 'inline-block',
|
|
|
|
|
|
maxWidth: '180px',
|
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
|
textOverflow: 'ellipsis',
|
|
|
|
|
|
whiteSpace: 'nowrap'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
{value || '-'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
),
|
2026-01-30 14:01:23 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '操作',
|
|
|
|
|
|
key: 'action',
|
|
|
|
|
|
width: 120,
|
|
|
|
|
|
fixed: 'right',
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
{record.isEditable && (
|
|
|
|
|
|
<Tooltip title="编辑">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="link"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
icon={<EditOutlined />}
|
|
|
|
|
|
onClick={() => handleEdit(record)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<Tooltip title="查看历史">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="link"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
icon={<EyeOutlined />}
|
|
|
|
|
|
onClick={() => handleViewHistory(record)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</Space>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
),
|
|
|
|
|
|
},
|
2025-12-24 16:44:27 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
|
|
|
|
|
const response = await axios.get('/api/consumables/logs/export', {
|
2025-12-24 16:44:27 +08:00
|
|
|
|
params,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
responseType: 'blob',
|
2025-12-24 16:44:27 +08:00
|
|
|
|
});
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2025-12-24 16:44:27 +08:00
|
|
|
|
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();
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2025-12-24 16:44:27 +08:00
|
|
|
|
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');
|
|
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
|
|
|
|
|
const response = await axios.get('/api/consumables/logs', {
|
|
|
|
|
|
params: { ...params, page: 1, pageSize: 10000 },
|
2025-12-24 16:44:27 +08:00
|
|
|
|
});
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2025-12-24 16:44:27 +08:00
|
|
|
|
const exportData = response.data.logs.map(log => ({
|
2026-02-10 11:04:01 +08:00
|
|
|
|
时间: 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 || '',
|
2026-02-24 11:58:31 +08:00
|
|
|
|
耗材状态: log.isConsumableDeleted ? '已删除' : '正常',
|
|
|
|
|
|
分类: log.consumableSnapshot?.category || '',
|
|
|
|
|
|
单位: log.consumableSnapshot?.unit || '',
|
|
|
|
|
|
单价: log.consumableSnapshot?.unitPrice || '',
|
2025-12-24 16:44:27 +08:00
|
|
|
|
}));
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2025-12-24 16:44:27 +08:00
|
|
|
|
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`);
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2025-12-24 16:44:27 +08:00
|
|
|
|
message.success('导出Excel成功');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('导出Excel失败');
|
|
|
|
|
|
console.error('导出Excel失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const getOperationTypeText = type => {
|
2025-12-24 16:44:27 +08:00
|
|
|
|
const map = {
|
2026-02-10 11:04:01 +08:00
|
|
|
|
in: '入库',
|
|
|
|
|
|
out: '出库',
|
|
|
|
|
|
create: '创建',
|
|
|
|
|
|
update: '更新',
|
|
|
|
|
|
delete: '删除',
|
|
|
|
|
|
adjust: '调整',
|
|
|
|
|
|
import: '导入',
|
2025-12-24 16:44:27 +08:00
|
|
|
|
};
|
|
|
|
|
|
return map[type] || type;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleImport = async file => {
|
2025-12-24 16:44:27 +08:00
|
|
|
|
setImporting(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const reader = new FileReader();
|
2026-02-10 11:04:01 +08:00
|
|
|
|
reader.onload = async e => {
|
2025-12-24 16:44:27 +08:00
|
|
|
|
try {
|
|
|
|
|
|
let logItems = [];
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2025-12-24 16:44:27 +08:00
|
|
|
|
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, ''));
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2025-12-24 16:44:27 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2025-12-24 16:44:27 +08:00
|
|
|
|
const response = await axios.post('/api/consumables/logs/import', {
|
|
|
|
|
|
logs: logItems,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
operator: '前端导入',
|
2025-12-24 16:44:27 +08:00
|
|
|
|
});
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2025-12-24 16:44:27 +08:00
|
|
|
|
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));
|
|
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2025-12-24 16:44:27 +08:00
|
|
|
|
setImportModalVisible(false);
|
|
|
|
|
|
fetchLogs(1, pagination.pageSize);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
message.error('解析文件失败: ' + err.message);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setImporting(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2025-12-24 16:44:27 +08:00
|
|
|
|
if (importType === 'excel') {
|
|
|
|
|
|
reader.readAsArrayBuffer(file);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
reader.readAsText(file);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('导入失败');
|
|
|
|
|
|
setImporting(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const downloadTemplate = () => {
|
|
|
|
|
|
const template = [
|
|
|
|
|
|
{
|
2026-02-10 11:04:01 +08:00
|
|
|
|
耗材ID: 'CON123456',
|
|
|
|
|
|
耗材名称: '示例耗材',
|
|
|
|
|
|
操作类型: '入库',
|
|
|
|
|
|
变动数量: 10,
|
|
|
|
|
|
操作前库存: 100,
|
|
|
|
|
|
操作后库存: 110,
|
|
|
|
|
|
操作人: '管理员',
|
|
|
|
|
|
原因: '示例原因',
|
|
|
|
|
|
备注: '示例备注',
|
|
|
|
|
|
},
|
2025-12-24 16:44:27 +08:00
|
|
|
|
];
|
2026-01-30 14:01:23 +08:00
|
|
|
|
|
2025-12-24 16:44:27 +08:00
|
|
|
|
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');
|
2026-01-30 14:01:23 +08:00
|
|
|
|
|
2025-12-24 16:44:27 +08:00
|
|
|
|
message.success('模板下载成功');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-30 14:01:23 +08:00
|
|
|
|
// 编辑日志
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleEdit = record => {
|
2026-01-30 14:01:23 +08:00
|
|
|
|
setCurrentLog(record);
|
|
|
|
|
|
form.setFieldsValue({
|
|
|
|
|
|
reason: record.reason,
|
|
|
|
|
|
notes: record.notes,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
modificationReason: '',
|
2026-01-30 14:01:23 +08:00
|
|
|
|
});
|
|
|
|
|
|
setEditModalVisible(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-24 13:20:26 +08:00
|
|
|
|
// 查看归档详情
|
|
|
|
|
|
const handleViewArchive = async archiveId => {
|
|
|
|
|
|
setArchiveModalVisible(true);
|
|
|
|
|
|
setArchiveLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await axios.get(`/api/consumables/archives/${archiveId}`);
|
|
|
|
|
|
setCurrentArchive(response.data);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('获取归档详情失败');
|
|
|
|
|
|
console.error('获取归档详情失败:', error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setArchiveLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-30 14:01:23 +08:00
|
|
|
|
// 提交编辑
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleEditSubmit = async values => {
|
2026-01-30 14:01:23 +08:00
|
|
|
|
if (!currentLog) return;
|
|
|
|
|
|
|
|
|
|
|
|
setEditLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await axios.put(`/api/consumables/logs/${currentLog.id}`, {
|
|
|
|
|
|
reason: values.reason,
|
|
|
|
|
|
notes: values.notes,
|
|
|
|
|
|
operator: values.operator || '管理员',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
modificationReason: values.modificationReason,
|
2026-01-30 14:01:23 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
message.success('日志修改成功');
|
|
|
|
|
|
setEditModalVisible(false);
|
|
|
|
|
|
fetchLogs(pagination.current, pagination.pageSize);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error(error.response?.data?.error || '修改失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setEditLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 查看修改历史
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleViewHistory = async record => {
|
2026-01-30 14:01:23 +08:00
|
|
|
|
setCurrentLog(record);
|
|
|
|
|
|
setHistoryModalVisible(true);
|
|
|
|
|
|
setHistoryLoading(true);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await axios.get(`/api/consumables/logs/${record.id}/history`);
|
|
|
|
|
|
setLogHistory(response.data.history || []);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('获取修改历史失败');
|
|
|
|
|
|
setLogHistory([]);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setHistoryLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-24 16:44:27 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<div>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Card
|
2025-12-24 16:44:27 +08:00
|
|
|
|
title={
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
<FileTextOutlined />
|
|
|
|
|
|
<span>耗材操作日志</span>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Card size="small" style={{ marginBottom: 16 }}>
|
|
|
|
|
|
<Space wrap>
|
2026-02-26 11:24:00 +08:00
|
|
|
|
<Input
|
2026-03-05 10:41:49 +08:00
|
|
|
|
placeholder={inputPlaceholders.consumableId}
|
|
|
|
|
|
style={{ ...inputStyles.search, width: 200 }}
|
2025-12-24 16:44:27 +08:00
|
|
|
|
allowClear
|
2026-02-26 11:24:00 +08:00
|
|
|
|
value={filters.consumableId}
|
|
|
|
|
|
onChange={e => handleFilterChange('consumableId', e.target.value)}
|
2025-12-24 16:44:27 +08:00
|
|
|
|
prefix={<SearchOutlined />}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Select
|
2026-03-05 09:57:44 +08:00
|
|
|
|
mode="multiple"
|
2025-12-24 16:44:27 +08:00
|
|
|
|
value={filters.operationType}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
onChange={value => handleFilterChange('operationType', value)}
|
2026-03-05 10:41:49 +08:00
|
|
|
|
style={{ ...selectStyles.base, width: 200 }}
|
2026-03-05 09:57:44 +08:00
|
|
|
|
placeholder="选择操作类型"
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
maxTagCount="responsive"
|
2025-12-24 16:44:27 +08:00
|
|
|
|
>
|
|
|
|
|
|
<Option value="in">入库</Option>
|
|
|
|
|
|
<Option value="out">出库</Option>
|
|
|
|
|
|
<Option value="create">创建</Option>
|
|
|
|
|
|
<Option value="update">更新</Option>
|
|
|
|
|
|
<Option value="delete">删除</Option>
|
|
|
|
|
|
<Option value="adjust">调整</Option>
|
|
|
|
|
|
<Option value="import">导入</Option>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<RangePicker
|
|
|
|
|
|
value={filters.dateRange}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
onChange={dates => handleFilterChange('dateRange', dates)}
|
2026-03-05 10:41:49 +08:00
|
|
|
|
placeholder={inputPlaceholders.dateRange}
|
|
|
|
|
|
style={datePickerStyles.range}
|
2025-12-24 16:44:27 +08:00
|
|
|
|
/>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
icon={<HistoryOutlined />}
|
2025-12-24 16:44:27 +08:00
|
|
|
|
onClick={() => {
|
2026-03-05 09:57:44 +08:00
|
|
|
|
setFilters({ operationType: ['in', 'out'], consumableId: '', dateRange: null });
|
2025-12-24 16:44:27 +08:00
|
|
|
|
fetchLogs(1, pagination.pageSize);
|
|
|
|
|
|
}}
|
2026-03-05 10:41:49 +08:00
|
|
|
|
style={{ height: '40px', borderRadius: '10px' }}
|
2025-12-24 16:44:27 +08:00
|
|
|
|
>
|
|
|
|
|
|
重置筛选
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Dropdown
|
|
|
|
|
|
menu={{
|
|
|
|
|
|
items: [
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'csv',
|
|
|
|
|
|
icon: <FileOutlined />,
|
|
|
|
|
|
label: '导出CSV',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
onClick: () => handleExport(filters),
|
2025-12-24 16:44:27 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'excel',
|
|
|
|
|
|
icon: <FileExcelOutlined />,
|
|
|
|
|
|
label: '导出Excel',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
onClick: () => handleExportExcel(filters),
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
2025-12-24 16:44:27 +08:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-05 10:41:49 +08:00
|
|
|
|
<Button icon={<DownloadOutlined />} style={{ height: '40px', borderRadius: '10px' }}>
|
2025-12-24 16:44:27 +08:00
|
|
|
|
导出 <DownOutlined />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Dropdown>
|
2026-03-05 10:41:49 +08:00
|
|
|
|
<Button icon={<UploadOutlined />} onClick={() => setImportModalVisible(true)} style={{ height: '40px', borderRadius: '10px' }}>
|
2025-12-24 16:44:27 +08:00
|
|
|
|
导入
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<Table
|
|
|
|
|
|
columns={columns}
|
|
|
|
|
|
dataSource={logs}
|
|
|
|
|
|
rowKey="id"
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
pagination={{
|
|
|
|
|
|
...pagination,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
showTotal: total => `共 ${total} 条记录`,
|
2025-12-24 16:44:27 +08:00
|
|
|
|
showSizeChanger: true,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
showQuickJumper: true,
|
2025-12-24 16:44:27 +08:00
|
|
|
|
}}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
onChange={pagination => fetchLogs(pagination.current, pagination.pageSize)}
|
2025-12-24 16:44:27 +08:00
|
|
|
|
scroll={{ x: 1500 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
title="导入操作日志"
|
|
|
|
|
|
open={importModalVisible}
|
2026-03-12 12:40:12 +08:00
|
|
|
|
closeIcon={<CloseButton />}
|
2025-12-24 16:44:27 +08:00
|
|
|
|
onCancel={() => {
|
|
|
|
|
|
setImportModalVisible(false);
|
|
|
|
|
|
setImportType('excel');
|
|
|
|
|
|
}}
|
|
|
|
|
|
footer={null}
|
|
|
|
|
|
width={500}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div style={{ marginBottom: 16 }}>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Radio.Group
|
|
|
|
|
|
value={importType}
|
|
|
|
|
|
onChange={e => setImportType(e.target.value)}
|
2025-12-24 16:44:27 +08:00
|
|
|
|
style={{ marginBottom: 16 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Radio.Button value="excel">Excel文件</Radio.Button>
|
|
|
|
|
|
<Radio.Button value="csv">CSV文件</Radio.Button>
|
|
|
|
|
|
</Radio.Group>
|
|
|
|
|
|
</div>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2025-12-24 16:44:27 +08:00
|
|
|
|
<div style={{ marginBottom: 16 }}>
|
|
|
|
|
|
<Button type="link" onClick={downloadTemplate}>
|
|
|
|
|
|
下载导入模板
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<span style={{ color: '#888', marginLeft: 8 }}>(建议先下载模板填写)</span>
|
|
|
|
|
|
</div>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2025-12-24 16:44:27 +08:00
|
|
|
|
<Upload
|
|
|
|
|
|
accept={importType === 'excel' ? '.xlsx,.xls' : '.csv'}
|
|
|
|
|
|
showUploadList={false}
|
|
|
|
|
|
beforeUpload={handleImport}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Button type="primary" icon={<UploadOutlined />} loading={importing}>
|
|
|
|
|
|
选择{importType === 'excel' ? 'Excel' : 'CSV'}文件并导入
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Upload>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2025-12-24 16:44:27 +08:00
|
|
|
|
<div style={{ marginTop: 16, color: '#888', fontSize: 12 }}>
|
|
|
|
|
|
<p>注意事项:</p>
|
|
|
|
|
|
<ul>
|
|
|
|
|
|
<li>支持 Excel (.xlsx, .xls) 或 CSV 格式</li>
|
|
|
|
|
|
<li>文件列名必须包含:耗材ID、操作类型</li>
|
|
|
|
|
|
<li>操作类型支持:入库、出库、创建、更新、删除、调整、导入</li>
|
|
|
|
|
|
<li>系统将根据筛选条件过滤需要导出的数据</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Modal>
|
2026-01-30 14:01:23 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 编辑日志弹窗 */}
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
title="编辑日志记录"
|
|
|
|
|
|
open={editModalVisible}
|
2026-03-12 12:40:12 +08:00
|
|
|
|
closeIcon={<CloseButton />}
|
2026-01-30 14:01:23 +08:00
|
|
|
|
onCancel={() => {
|
|
|
|
|
|
setEditModalVisible(false);
|
|
|
|
|
|
form.resetFields();
|
|
|
|
|
|
}}
|
|
|
|
|
|
onOk={() => form.submit()}
|
|
|
|
|
|
confirmLoading={editLoading}
|
|
|
|
|
|
width={600}
|
|
|
|
|
|
>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Form form={form} layout="vertical" onFinish={handleEditSubmit}>
|
|
|
|
|
|
<Form.Item label="操作原因" name="reason">
|
2026-03-05 10:41:49 +08:00
|
|
|
|
<Input.TextArea rows={2} placeholder={inputPlaceholders.reason} style={textAreaStyles.base} />
|
2026-01-30 14:01:23 +08:00
|
|
|
|
</Form.Item>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Form.Item label="备注" name="notes">
|
2026-03-05 10:41:49 +08:00
|
|
|
|
<Input.TextArea rows={3} placeholder={inputPlaceholders.notes} style={textAreaStyles.base} />
|
2026-01-30 14:01:23 +08:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
label="修改原因"
|
|
|
|
|
|
name="modificationReason"
|
2026-03-05 10:41:49 +08:00
|
|
|
|
rules={[inputValidationRules.required('请输入修改原因')]}
|
2026-01-30 14:01:23 +08:00
|
|
|
|
>
|
2026-03-05 10:41:49 +08:00
|
|
|
|
<Input.TextArea rows={2} placeholder="请输入修改原因(必填)" style={textAreaStyles.base} />
|
2026-01-30 14:01:23 +08:00
|
|
|
|
</Form.Item>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Form.Item label="修改人" name="operator">
|
2026-03-05 10:41:49 +08:00
|
|
|
|
<Input placeholder={inputPlaceholders.operator} style={inputStyles.form} />
|
2026-01-30 14:01:23 +08:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 修改历史弹窗 */}
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
title="日志修改历史"
|
|
|
|
|
|
open={historyModalVisible}
|
2026-03-12 12:40:12 +08:00
|
|
|
|
closeIcon={<CloseButton />}
|
2026-01-30 14:01:23 +08:00
|
|
|
|
onCancel={() => {
|
|
|
|
|
|
setHistoryModalVisible(false);
|
|
|
|
|
|
setLogHistory([]);
|
|
|
|
|
|
}}
|
|
|
|
|
|
footer={null}
|
|
|
|
|
|
width={700}
|
|
|
|
|
|
>
|
|
|
|
|
|
{historyLoading ? (
|
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '40px' }}>加载中...</div>
|
|
|
|
|
|
) : logHistory.length <= 1 ? (
|
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '40px', color: '#888' }}>
|
|
|
|
|
|
该记录暂无修改历史
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Timeline mode="left">
|
|
|
|
|
|
{logHistory.map((item, index) => (
|
|
|
|
|
|
<Timeline.Item
|
|
|
|
|
|
key={item.id}
|
|
|
|
|
|
color={index === logHistory.length - 1 ? 'green' : 'blue'}
|
|
|
|
|
|
label={dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss')}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div style={{ marginBottom: 8 }}>
|
|
|
|
|
|
<Tag color={getOperationTag(item.operationType).props.color}>
|
|
|
|
|
|
{getOperationTag(item.operationType).props.children}
|
|
|
|
|
|
</Tag>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
{item.modifiedBy && <Tag color="orange">已修改</Tag>}
|
2026-01-30 14:01:23 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ fontSize: 12, color: '#666' }}>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<p>
|
|
|
|
|
|
<strong>耗材:</strong> {item.consumableName} ({item.consumableId})
|
2026-02-24 11:58:31 +08:00
|
|
|
|
{item.isConsumableDeleted && <Tag color="red" style={{ marginLeft: 8 }}>已删除</Tag>}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
<strong>操作人:</strong> {item.operator}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{item.reason && (
|
|
|
|
|
|
<p>
|
|
|
|
|
|
<strong>原因:</strong> {item.reason}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{item.notes && (
|
|
|
|
|
|
<p>
|
|
|
|
|
|
<strong>备注:</strong> {item.notes}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
2026-02-24 11:58:31 +08:00
|
|
|
|
{item.consumableSnapshot && (
|
|
|
|
|
|
<p>
|
2026-02-24 13:20:26 +08:00
|
|
|
|
<strong>快照信息:</strong> 分类:{item.consumableSnapshot.category || '-'} |
|
|
|
|
|
|
单位:{item.consumableSnapshot.unit || '-'} |
|
2026-02-24 11:58:31 +08:00
|
|
|
|
单价:{item.consumableSnapshot.unitPrice || '-'}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
2026-01-30 14:01:23 +08:00
|
|
|
|
{item.modifiedBy && (
|
|
|
|
|
|
<>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<p>
|
|
|
|
|
|
<strong>修改人:</strong> {item.modifiedBy}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
<strong>修改时间:</strong>{' '}
|
|
|
|
|
|
{dayjs(item.modifiedAt).format('YYYY-MM-DD HH:mm:ss')}
|
|
|
|
|
|
</p>
|
2026-01-30 14:01:23 +08:00
|
|
|
|
{item.modificationReason && (
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<p>
|
|
|
|
|
|
<strong>修改原因:</strong> {item.modificationReason}
|
|
|
|
|
|
</p>
|
2026-01-30 14:01:23 +08:00
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Timeline.Item>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Timeline>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Modal>
|
2026-02-24 13:20:26 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 归档详情弹窗 */}
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
title="归档记录详情"
|
|
|
|
|
|
open={archiveModalVisible}
|
2026-03-12 12:40:12 +08:00
|
|
|
|
closeIcon={<CloseButton />}
|
2026-02-24 13:20:26 +08:00
|
|
|
|
onCancel={() => {
|
|
|
|
|
|
setArchiveModalVisible(false);
|
|
|
|
|
|
setCurrentArchive(null);
|
|
|
|
|
|
}}
|
|
|
|
|
|
footer={null}
|
|
|
|
|
|
width={600}
|
|
|
|
|
|
>
|
|
|
|
|
|
{archiveLoading ? (
|
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '40px' }}>加载中...</div>
|
|
|
|
|
|
) : !currentArchive ? (
|
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '40px', color: '#888' }}>
|
|
|
|
|
|
无法获取归档信息
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Card size="small" title="基本信息" style={{ marginBottom: 16 }}>
|
|
|
|
|
|
<p><strong>归档ID:</strong> <code>{currentArchive.archiveId}</code></p>
|
|
|
|
|
|
<p><strong>耗材ID:</strong> <code>{currentArchive.consumableId}</code></p>
|
|
|
|
|
|
<p><strong>耗材名称:</strong> {currentArchive.consumableName}</p>
|
|
|
|
|
|
<p><strong>删除人:</strong> {currentArchive.deletedBy}</p>
|
|
|
|
|
|
<p><strong>删除时间:</strong> {dayjs(currentArchive.deletedAt).format('YYYY-MM-DD HH:mm:ss')}</p>
|
|
|
|
|
|
<p><strong>删除原因:</strong> {currentArchive.deleteReason || '-'}</p>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<Card size="small" title="操作统计" style={{ marginBottom: 16 }}>
|
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col span={8}>
|
|
|
|
|
|
<Statistic title="总操作数" value={currentArchive.totalOperations} />
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={8}>
|
|
|
|
|
|
<Statistic title="总入库" value={currentArchive.totalInQuantity} />
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={8}>
|
|
|
|
|
|
<Statistic title="总出库" value={currentArchive.totalOutQuantity} />
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
<Divider style={{ margin: '12px 0' }} />
|
|
|
|
|
|
<p><strong>首次操作:</strong> {currentArchive.firstOperationAt ? dayjs(currentArchive.firstOperationAt).format('YYYY-MM-DD HH:mm:ss') : '-'}</p>
|
|
|
|
|
|
<p><strong>最后操作:</strong> {currentArchive.lastOperationAt ? dayjs(currentArchive.lastOperationAt).format('YYYY-MM-DD HH:mm:ss') : '-'}</p>
|
|
|
|
|
|
<p><strong>删除时库存:</strong> {currentArchive.finalStock}</p>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{currentArchive.consumableSnapshot && (
|
|
|
|
|
|
<Card size="small" title="耗材快照">
|
|
|
|
|
|
<p><strong>分类:</strong> {currentArchive.consumableSnapshot.category || '-'}</p>
|
|
|
|
|
|
<p><strong>单位:</strong> {currentArchive.consumableSnapshot.unit || '-'}</p>
|
|
|
|
|
|
<p><strong>单价:</strong> {currentArchive.consumableSnapshot.unitPrice || '-'}</p>
|
|
|
|
|
|
<p><strong>供应商:</strong> {currentArchive.consumableSnapshot.supplier || '-'}</p>
|
|
|
|
|
|
<p><strong>位置:</strong> {currentArchive.consumableSnapshot.location || '-'}</p>
|
|
|
|
|
|
<p><strong>最小库存:</strong> {currentArchive.consumableSnapshot.minStock || '-'}</p>
|
|
|
|
|
|
<p><strong>最大库存:</strong> {currentArchive.consumableSnapshot.maxStock || '-'}</p>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Modal>
|
2025-12-24 16:44:27 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default ConsumableLogs;
|