feat(consumables): 添加手动创建耗材日志功能

后端:
- 新增 POST /logs 接口用于创建耗材操作日志
- 添加操作类型验证和必填字段检查
- 支持自定义创建时间

前端:
- 添加手动创建日志弹窗和表单
- 实现表单提交和验证逻辑
- 提供操作类型选择器和库存信息输入
This commit is contained in:
zhang1106
2026-04-09 15:27:48 +08:00
parent bfe6ca4744
commit 585e044649
2 changed files with 304 additions and 0 deletions
+73
View File
@@ -8,6 +8,7 @@ const ConsumableRecord = require('../models/ConsumableRecord');
const ConsumableLog = require('../models/ConsumableLog');
const ConsumableLogArchive = require('../models/ConsumableLogArchive');
const { PAGINATION, RETRY } = require('../config');
const { generateId } = require('../utils/idGenerator');
router.get('/', async (req, res) => {
try {
@@ -1045,6 +1046,78 @@ router.get('/logs/export', async (req, res) => {
}
});
router.post('/logs', async (req, res) => {
const transaction = await sequelize.transaction();
try {
const {
consumableId,
consumableName,
operationType,
quantity,
previousStock,
currentStock,
operator,
reason,
notes,
snList,
deviceId,
deviceName,
rackId,
rackName,
roomId,
roomName,
createdAt,
} = req.body;
if (!consumableName || !operationType) {
await transaction.rollback();
return res.status(400).json({ error: '缺少必填字段:耗材名称、操作类型' });
}
const validOperationTypes = ['in', 'out', 'create', 'update', 'delete', 'adjust', 'import'];
if (!validOperationTypes.includes(operationType)) {
await transaction.rollback();
return res.status(400).json({ error: '无效的操作类型' });
}
const logData = {
consumableId: consumableId || generateId({ prefix: 'LOG' }),
consumableName,
operationType,
quantity: parseInt(quantity) || 0,
previousStock: parseInt(previousStock) || 0,
currentStock: parseInt(currentStock) || 0,
operator: operator || '',
reason: reason || '',
notes: notes || '',
snList: Array.isArray(snList) ? snList : [],
deviceId: deviceId || null,
deviceName: deviceName || null,
rackId: rackId || null,
rackName: rackName || null,
roomId: roomId || null,
roomName: roomName || null,
isEditable: true,
};
if (createdAt) {
const parsedDate = new Date(createdAt);
if (!isNaN(parsedDate.getTime())) {
logData.createdAt = parsedDate;
logData.updatedAt = parsedDate;
}
}
const log = await ConsumableLog.create(logData, { transaction });
await transaction.commit();
res.status(201).json({ message: '日志创建成功', log });
} catch (error) {
await transaction.rollback();
res.status(500).json({ error: error.message });
}
});
router.post('/logs/import', async (req, res) => {
const transaction = await sequelize.transaction();
try {
+231
View File
@@ -34,6 +34,7 @@ import {
DownOutlined,
EditOutlined,
EyeOutlined,
PlusOutlined,
} from '@ant-design/icons';
import axios from 'axios';
import CloseButton from '../components/CloseButton';
@@ -79,6 +80,11 @@ function ConsumableLogs() {
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);
@@ -581,6 +587,43 @@ function ConsumableLogs() {
}
};
// 手动添加日志
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
@@ -656,6 +699,14 @@ function ConsumableLogs() {
导出 <DownOutlined />
</Button>
</Dropdown>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleAddLog}
style={{ height: '40px', borderRadius: '10px' }}
>
手动添加
</Button>
<Button
icon={<UploadOutlined />}
onClick={() => setImportModalVisible(true)}
@@ -961,6 +1012,186 @@ function ConsumableLogs() {
</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>
);
}