feat(consumables): 添加手动创建耗材日志功能
后端: - 新增 POST /logs 接口用于创建耗材操作日志 - 添加操作类型验证和必填字段检查 - 支持自定义创建时间 前端: - 添加手动创建日志弹窗和表单 - 实现表单提交和验证逻辑 - 提供操作类型选择器和库存信息输入
This commit is contained in:
@@ -8,6 +8,7 @@ const ConsumableRecord = require('../models/ConsumableRecord');
|
|||||||
const ConsumableLog = require('../models/ConsumableLog');
|
const ConsumableLog = require('../models/ConsumableLog');
|
||||||
const ConsumableLogArchive = require('../models/ConsumableLogArchive');
|
const ConsumableLogArchive = require('../models/ConsumableLogArchive');
|
||||||
const { PAGINATION, RETRY } = require('../config');
|
const { PAGINATION, RETRY } = require('../config');
|
||||||
|
const { generateId } = require('../utils/idGenerator');
|
||||||
|
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
try {
|
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) => {
|
router.post('/logs/import', async (req, res) => {
|
||||||
const transaction = await sequelize.transaction();
|
const transaction = await sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
DownOutlined,
|
DownOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
|
PlusOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import CloseButton from '../components/CloseButton';
|
import CloseButton from '../components/CloseButton';
|
||||||
@@ -79,6 +80,11 @@ function ConsumableLogs() {
|
|||||||
const [currentArchive, setCurrentArchive] = useState(null);
|
const [currentArchive, setCurrentArchive] = useState(null);
|
||||||
const [archiveLoading, setArchiveLoading] = useState(false);
|
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) => {
|
const fetchLogs = async (page = 1, pageSize = 10, currentFilters = filters) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Card
|
<Card
|
||||||
@@ -656,6 +699,14 @@ function ConsumableLogs() {
|
|||||||
导出 <DownOutlined />
|
导出 <DownOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={handleAddLog}
|
||||||
|
style={{ height: '40px', borderRadius: '10px' }}
|
||||||
|
>
|
||||||
|
手动添加
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
icon={<UploadOutlined />}
|
icon={<UploadOutlined />}
|
||||||
onClick={() => setImportModalVisible(true)}
|
onClick={() => setImportModalVisible(true)}
|
||||||
@@ -961,6 +1012,186 @@ function ConsumableLogs() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user