Files
yunrui_asset/frontend/src/pages/ConsumableLogs.jsx
T
zhang1106 6a7e52312e feat(日志): 添加删除日志记录功能
后端添加删除日志的API接口,前端在日志列表中添加删除按钮和确认弹窗
2026-04-09 16:27:20 +08:00

1230 lines
39 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
Popconfirm,
} from 'antd';
import {
HistoryOutlined,
SearchOutlined,
FileTextOutlined,
DownloadOutlined,
UploadOutlined,
FileExcelOutlined,
FileOutlined,
DownOutlined,
EditOutlined,
EyeOutlined,
PlusOutlined,
DeleteOutlined,
} 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 [addModalVisible, setAddModalVisible] = useState(false);
const [addLoading, setAddLoading] = useState(false);
const [addForm] = Form.useForm();
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 <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),
render: date => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
},
{
title: '耗材ID',
dataIndex: 'consumableId',
key: 'consumableId',
width: 150,
render: value => <code>{value}</code>,
},
{
title: '耗材名称',
dataIndex: 'consumableName',
key: 'consumableName',
width: 180,
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
}
>
<Space direction="vertical" size={0}>
<span>{value}</span>
{record.isConsumableDeleted && (
<Tag color="red" size="small">
已删除
</Tag>
)}
</Space>
</Tooltip>
),
},
{
title: '操作类型',
dataIndex: 'operationType',
key: 'operationType',
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>
),
},
{
title: '变动数量',
dataIndex: 'quantity',
key: 'quantity',
width: 100,
render: (value, record) => (
<span
style={{
color: value > 0 ? '#52c41a' : value < 0 ? '#ff4d4f' : '#888',
fontWeight: 'bold',
}}
>
{value > 0 ? '+' : ''}
{value}
</span>
),
},
{
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 (
<Space size={2} wrap>
{snListArray.map((sn, index) => (
<Tag key={index} color="purple" style={{ fontSize: '11px' }}>
{sn}
</Tag>
))}
</Space>
);
}
const content = (
<div style={{ maxHeight: '200px', overflowY: 'auto', maxWidth: '300px' }}>
{snListArray.map((sn, index) => (
<Tag key={index} color="purple" style={{ marginBottom: '4px', fontSize: '11px' }}>
{sn}
</Tag>
))}
</div>
);
return (
<Popover content={content} title={`SN列表 (${snListArray.length}个)`} trigger="click">
<Tag color="purple" style={{ cursor: 'pointer' }}>
{snListArray.length} 个SN 🔍
</Tag>
</Popover>
);
},
},
{
title: '备注',
dataIndex: 'notes',
key: 'notes',
width: 200,
render: value => (
<Tooltip title={value || '-'}>
<span
style={{
display: 'inline-block',
maxWidth: '180px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{value || '-'}
</span>
</Tooltip>
),
},
{
title: '操作',
key: 'action',
width: 150,
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>
<Popconfirm
title="确认删除"
description="确定要删除这条日志记录吗?此操作不可恢复。"
onConfirm={() => handleDeleteLog(record.id)}
okText="删除"
cancelText="取消"
okButtonProps={{ danger: true }}
>
<Tooltip title="删除">
<Button
type="link"
size="small"
danger
icon={<DeleteOutlined />}
/>
</Tooltip>
</Popconfirm>
</Space>
),
},
];
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 || '',
耗材状态: log.isConsumableDeleted ? '已删除' : '正常',
分类: log.consumableSnapshot?.category || '',
单位: log.consumableSnapshot?.unit || '',
单价: log.consumableSnapshot?.unitPrice || '',
}));
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('模板下载成功');
};
// 编辑日志
const handleEdit = record => {
setCurrentLog(record);
form.setFieldsValue({
reason: record.reason,
notes: record.notes,
modificationReason: '',
});
setEditModalVisible(true);
};
// 查看归档详情
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);
}
};
// 提交编辑
const handleEditSubmit = async values => {
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 || '管理员',
modificationReason: values.modificationReason,
});
message.success('日志修改成功');
setEditModalVisible(false);
fetchLogs(pagination.current, pagination.pageSize);
} catch (error) {
message.error(error.response?.data?.error || '修改失败');
} finally {
setEditLoading(false);
}
};
// 查看修改历史
const handleViewHistory = async record => {
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);
}
};
// 删除日志
const handleDeleteLog = async logId => {
try {
await axios.delete(`/api/consumables/logs/${logId}`);
message.success('日志删除成功');
fetchLogs(pagination.current, pagination.pageSize);
} catch (error) {
message.error(error.response?.data?.error || '删除失败');
}
};
// 手动添加日志
const handleAddLog = () => {
addForm.resetFields();
setAddModalVisible(true);
};
const handleAddSubmit = async values => {
setAddLoading(true);
try {
const submitData = {
consumableId: values.consumableId,
consumableName: values.consumableName,
operationType: values.operationType,
quantity: values.quantity || 0,
previousStock: values.previousStock || 0,
currentStock: values.currentStock || 0,
operator: values.operator || '',
reason: values.reason || '',
notes: values.notes || '',
};
if (values.createdAt) {
submitData.createdAt = values.createdAt.format('YYYY-MM-DD HH:mm:ss');
}
await axios.post('/api/consumables/logs', submitData);
message.success('日志添加成功');
setAddModalVisible(false);
fetchLogs(1, pagination.pageSize);
} catch (error) {
message.error(error.response?.data?.error || '添加失败');
} finally {
setAddLoading(false);
}
};
return (
<div>
<Card
title={
<Space>
<FileTextOutlined />
<span>耗材操作日志</span>
</Space>
}
>
<Card size="small" style={{ marginBottom: 16 }}>
<Space wrap>
<Input
placeholder={inputPlaceholders.consumableId}
style={{ ...inputStyles.search, width: 200 }}
allowClear
value={filters.consumableId}
onChange={e => handleFilterChange('consumableId', e.target.value)}
prefix={<SearchOutlined />}
/>
<Select
mode="multiple"
value={filters.operationType}
onChange={value => handleFilterChange('operationType', value)}
style={{ ...selectStyles.base, width: 200 }}
placeholder="选择操作类型"
allowClear
maxTagCount="responsive"
>
<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}
onChange={dates => handleFilterChange('dateRange', dates)}
placeholder={inputPlaceholders.dateRange}
style={datePickerStyles.range}
/>
<Button
icon={<HistoryOutlined />}
onClick={() => {
setFilters({ operationType: ['in', 'out'], consumableId: '', dateRange: null });
fetchLogs(1, pagination.pageSize);
}}
style={{ height: '40px', borderRadius: '10px' }}
>
重置筛选
</Button>
<Dropdown
menu={{
items: [
{
key: 'csv',
icon: <FileOutlined />,
label: '导出CSV',
onClick: () => handleExport(filters),
},
{
key: 'excel',
icon: <FileExcelOutlined />,
label: '导出Excel',
onClick: () => handleExportExcel(filters),
},
],
}}
>
<Button icon={<DownloadOutlined />} style={{ height: '40px', borderRadius: '10px' }}>
导出 <DownOutlined />
</Button>
</Dropdown>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleAddLog}
style={{ height: '40px', borderRadius: '10px' }}
>
手动添加
</Button>
<Button
icon={<UploadOutlined />}
onClick={() => setImportModalVisible(true)}
style={{ height: '40px', borderRadius: '10px' }}
>
导入
</Button>
</Space>
</Card>
<Table
columns={columns}
dataSource={logs}
rowKey="id"
loading={loading}
pagination={{
...pagination,
showTotal: total => `共 ${total} 条记录`,
showSizeChanger: true,
showQuickJumper: true,
}}
onChange={pagination => fetchLogs(pagination.current, pagination.pageSize)}
scroll={{ x: 1500 }}
/>
</Card>
<Modal
title="导入操作日志"
open={importModalVisible}
closeIcon={<CloseButton />}
onCancel={() => {
setImportModalVisible(false);
setImportType('excel');
}}
footer={null}
width={500}
>
<div style={{ marginBottom: 16 }}>
<Radio.Group
value={importType}
onChange={e => setImportType(e.target.value)}
style={{ marginBottom: 16 }}
>
<Radio.Button value="excel">Excel文件</Radio.Button>
<Radio.Button value="csv">CSV文件</Radio.Button>
</Radio.Group>
</div>
<div style={{ marginBottom: 16 }}>
<Button type="link" onClick={downloadTemplate}>
下载导入模板
</Button>
<span style={{ color: '#888', marginLeft: 8 }}>建议先下载模板填写</span>
</div>
<Upload
accept={importType === 'excel' ? '.xlsx,.xls' : '.csv'}
showUploadList={false}
beforeUpload={handleImport}
>
<Button type="primary" icon={<UploadOutlined />} loading={importing}>
选择{importType === 'excel' ? 'Excel' : 'CSV'}文件并导入
</Button>
</Upload>
<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>
{/* 编辑日志弹窗 */}
<Modal
title="编辑日志记录"
open={editModalVisible}
closeIcon={<CloseButton />}
onCancel={() => {
setEditModalVisible(false);
form.resetFields();
}}
onOk={() => form.submit()}
confirmLoading={editLoading}
width={600}
>
<Form form={form} layout="vertical" onFinish={handleEditSubmit}>
<Form.Item label="操作原因" name="reason">
<Input.TextArea
rows={2}
placeholder={inputPlaceholders.reason}
style={textAreaStyles.base}
/>
</Form.Item>
<Form.Item label="备注" name="notes">
<Input.TextArea
rows={3}
placeholder={inputPlaceholders.notes}
style={textAreaStyles.base}
/>
</Form.Item>
<Form.Item
label="修改原因"
name="modificationReason"
rules={[inputValidationRules.required('请输入修改原因')]}
>
<Input.TextArea
rows={2}
placeholder="请输入修改原因(必填)"
style={textAreaStyles.base}
/>
</Form.Item>
<Form.Item label="修改人" name="operator">
<Input placeholder={inputPlaceholders.operator} style={inputStyles.form} />
</Form.Item>
</Form>
</Modal>
{/* 修改历史弹窗 */}
<Modal
title="日志修改历史"
open={historyModalVisible}
closeIcon={<CloseButton />}
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>
{item.modifiedBy && <Tag color="orange">已修改</Tag>}
</div>
<div style={{ fontSize: 12, color: '#666' }}>
<p>
<strong>耗材:</strong> {item.consumableName} ({item.consumableId})
{item.isConsumableDeleted && (
<Tag color="red" style={{ marginLeft: 8 }}>
已删除
</Tag>
)}
</p>
<p>
<strong>操作人:</strong> {item.operator}
</p>
{item.reason && (
<p>
<strong>原因:</strong> {item.reason}
</p>
)}
{item.notes && (
<p>
<strong>备注:</strong> {item.notes}
</p>
)}
{item.consumableSnapshot && (
<p>
<strong>快照信息:</strong> 分类:{item.consumableSnapshot.category || '-'} |
单位:{item.consumableSnapshot.unit || '-'} | 单价:
{item.consumableSnapshot.unitPrice || '-'}
</p>
)}
{item.modifiedBy && (
<>
<p>
<strong>修改人:</strong> {item.modifiedBy}
</p>
<p>
<strong>修改时间:</strong>{' '}
{dayjs(item.modifiedAt).format('YYYY-MM-DD HH:mm:ss')}
</p>
{item.modificationReason && (
<p>
<strong>修改原因:</strong> {item.modificationReason}
</p>
)}
</>
)}
</div>
</Timeline.Item>
))}
</Timeline>
)}
</Modal>
{/* 归档详情弹窗 */}
<Modal
title="归档记录详情"
open={archiveModalVisible}
closeIcon={<CloseButton />}
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>
{/* 手动添加日志弹窗 */}
<Modal
title={
<Space>
<FileTextOutlined style={{ color: '#6366f1' }} />
<span>手动添加日志</span>
</Space>
}
open={addModalVisible}
closeIcon={<CloseButton />}
onCancel={() => {
setAddModalVisible(false);
addForm.resetFields();
}}
footer={
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button
onClick={() => {
setAddModalVisible(false);
addForm.resetFields();
}}
>
取消
</Button>
<Button type="primary" loading={addLoading} onClick={() => addForm.submit()}>
确认添加
</Button>
</Space>
}
width={900}
destroyOnClose
>
<Form
form={addForm}
layout="vertical"
onFinish={handleAddSubmit}
initialValues={{
operationType: 'in',
quantity: 0,
previousStock: 0,
currentStock: 0,
}}
>
<Card
size="small"
title={
<Space>
<span style={{ fontSize: '14px', fontWeight: 500 }}>基本信息</span>
</Space>
}
style={{ marginBottom: 16 }}
>
<Row gutter={16}>
<Col span={24}>
<Form.Item
label={<Space><span style={{ color: '#ef4444' }}>*</span> 耗材名称</Space>}
name="consumableName"
rules={[inputValidationRules.required('请输入耗材名称')]}
>
<Input placeholder="请输入耗材名称" style={inputStyles.form} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label={<Space><span style={{ color: '#ef4444' }}>*</span> 操作类型</Space>}
name="operationType"
rules={[inputValidationRules.required('请选择操作类型')]}
>
<Select placeholder="请选择操作类型" style={{ width: '100%', height: '40px' }}>
<Option value="in">
<Space>
<Tag color="green">入库</Tag>
</Space>
</Option>
<Option value="out">
<Space>
<Tag color="red">出库</Tag>
</Space>
</Option>
<Option value="create">
<Space>
<Tag color="blue">创建</Tag>
</Space>
</Option>
<Option value="update">
<Space>
<Tag color="orange">更新</Tag>
</Space>
</Option>
<Option value="adjust">
<Space>
<Tag color="purple">调整</Tag>
</Space>
</Option>
<Option value="import">
<Space>
<Tag color="cyan">导入</Tag>
</Space>
</Option>
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="操作时间" name="createdAt">
<DatePicker
showTime
format="YYYY-MM-DD HH:mm:ss"
style={{ width: '100%', height: '40px' }}
placeholder="选择时间(默认当前时间)"
/>
</Form.Item>
</Col>
</Row>
</Card>
<Card
size="small"
title={
<Space>
<span style={{ fontSize: '14px', fontWeight: 500 }}>库存信息</span>
</Space>
}
style={{ marginBottom: 16 }}
>
<Row gutter={16}>
<Col span={8}>
<Form.Item label="变动数量" name="quantity">
<Input type="number" placeholder="变动数量" style={inputStyles.form} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="操作前库存" name="previousStock">
<Input type="number" placeholder="操作前" style={inputStyles.form} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="操作后库存" name="currentStock">
<Input type="number" placeholder="操作后" style={inputStyles.form} />
</Form.Item>
</Col>
</Row>
</Card>
<Card
size="small"
title={
<Space>
<span style={{ fontSize: '14px', fontWeight: 500 }}>其他信息</span>
</Space>
}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="操作人" name="operator">
<Input placeholder="请输入操作人" style={inputStyles.form} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="原因" name="reason">
<Input placeholder="请输入原因" style={inputStyles.form} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={24}>
<Form.Item label="备注" name="notes">
<Input.TextArea
rows={3}
placeholder="请输入备注信息(可选)"
style={textAreaStyles.base}
/>
</Form.Item>
</Col>
</Row>
</Card>
</Form>
</Modal>
</div>
);
}
export default ConsumableLogs;