From 9421e34147e0a1c3300e3390bcc036f81aabbab3 Mon Sep 17 00:00:00 2001
From: zhang1106 <849185023@qq.com>
Date: Thu, 9 Apr 2026 17:11:11 +0800
Subject: [PATCH] =?UTF-8?q?feat(=E8=80=97=E6=9D=90=E7=AE=A1=E7=90=86):=20?=
=?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=88=9B=E5=BB=BA=E8=80=97=E6=9D=90=E5=90=8C?=
=?UTF-8?q?=E6=97=B6=E5=85=A5=E5=BA=93=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98?=
=?UTF-8?q?=E5=8C=96=E5=BA=93=E5=AD=98=E7=BC=96=E8=BE=91=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
添加创建耗材时同时入库的功能,包含入库数量、操作人和原因等字段
强制初始化新建耗材的库存为0和空SN列表
禁止通过编辑接口直接修改库存和SN列表
前端新增同时入库选项及相关表单字段
优化编辑模式下库存信息的展示方式
---
backend/routes/consumables.js | 141 +++-
frontend/src/pages/ConsumableManagement.jsx | 817 ++++++++++++--------
2 files changed, 632 insertions(+), 326 deletions(-)
diff --git a/backend/routes/consumables.js b/backend/routes/consumables.js
index 54e902f..1538af0 100644
--- a/backend/routes/consumables.js
+++ b/backend/routes/consumables.js
@@ -122,14 +122,9 @@ router.post('/', async (req, res) => {
const consumableData = {
...req.body,
consumableId: req.body.consumableId || `CON${Date.now()}`,
+ currentStock: 0, // 强制设为0
+ snList: [], // 强制设为空数组
};
- // SN 列表非空时,取手动填写的库存和 SN 数量中的较大值
- if (Array.isArray(consumableData.snList) && consumableData.snList.length > 0) {
- consumableData.currentStock = Math.max(
- Number(consumableData.currentStock) || 0,
- consumableData.snList.length
- );
- }
const consumable = await Consumable.create(consumableData, { transaction });
await ConsumableLog.create(
@@ -170,6 +165,132 @@ router.post('/', async (req, res) => {
}
});
+// 创建耗材并同时入库
+router.post('/create-with-inbound', async (req, res) => {
+ const transaction = await sequelize.transaction();
+ try {
+ const {
+ consumableId,
+ name,
+ category,
+ unit,
+ currentStock,
+ minStock,
+ maxStock,
+ unitPrice,
+ supplier,
+ location,
+ description,
+ status,
+ // 入库相关字段
+ inboundQuantity,
+ inboundOperator,
+ inboundReason,
+ inboundNotes,
+ inboundSnList,
+ } = req.body;
+
+ // 创建耗材
+ const consumable = await Consumable.create({
+ consumableId: consumableId || `CON${Date.now()}`,
+ name,
+ category,
+ unit: unit || '个',
+ currentStock: 0,
+ minStock: minStock || 10,
+ maxStock: maxStock || 0,
+ unitPrice: unitPrice || 0,
+ supplier: supplier || '',
+ location: location || '',
+ description: description || '',
+ status: status || 'active',
+ snList: [],
+ }, { transaction });
+
+ // 记录创建日志
+ await ConsumableLog.create({
+ consumableId: consumable.consumableId,
+ consumableName: consumable.name,
+ operationType: 'create',
+ quantity: 0,
+ previousStock: 0,
+ currentStock: 0,
+ operator: inboundOperator || '系统',
+ reason: '新建耗材',
+ notes: description || '',
+ consumableSnapshot: {
+ category: consumable.category,
+ unit: consumable.unit,
+ unitPrice: consumable.unitPrice,
+ supplier: consumable.supplier,
+ location: consumable.location,
+ minStock: consumable.minStock,
+ maxStock: consumable.maxStock,
+ },
+ }, { transaction });
+
+ // 执行入库操作
+ if (inboundQuantity && inboundQuantity > 0) {
+ const previousStock = 0;
+ const newStock = inboundQuantity;
+ let updatedSnList = [...(inboundSnList || [])];
+
+ await Consumable.update({
+ currentStock: newStock,
+ snList: updatedSnList,
+ version: sequelize.literal('version + 1'),
+ }, {
+ where: { consumableId: consumable.consumableId },
+ transaction,
+ });
+
+ await ConsumableRecord.create({
+ consumableId: consumable.consumableId,
+ type: 'in',
+ quantity: inboundQuantity,
+ previousStock,
+ currentStock: newStock,
+ operator: inboundOperator || '系统',
+ reason: inboundReason || '初始入库',
+ notes: inboundNotes || '',
+ snList: updatedSnList,
+ }, { transaction });
+
+ await ConsumableLog.create({
+ consumableId: consumable.consumableId,
+ consumableName: consumable.name,
+ operationType: 'in',
+ quantity: inboundQuantity,
+ previousStock,
+ currentStock: newStock,
+ operator: inboundOperator || '系统',
+ reason: inboundReason || '初始入库',
+ notes: inboundNotes || '',
+ snList: updatedSnList,
+ consumableSnapshot: {
+ category: consumable.category,
+ unit: consumable.unit,
+ unitPrice: consumable.unitPrice,
+ supplier: consumable.supplier,
+ location: consumable.location,
+ },
+ }, { transaction });
+ }
+
+ await transaction.commit();
+ res.status(201).json(consumable);
+ } catch (error) {
+ await transaction.rollback();
+ console.error('创建耗材并入库错误:', error);
+ if (error.name === 'SequelizeValidationError') {
+ const messages = error.errors.map(e => `${e.path}: ${e.message}`).join(', ');
+ res.status(400).json({ error: `Validation error: ${messages}` });
+ } else {
+ res.status(400).json({ error: error.message });
+ }
+ }
+});
+
router.post('/import', async (req, res) => {
const transaction = await sequelize.transaction();
try {
@@ -1272,9 +1393,9 @@ router.put('/:id', async (req, res) => {
const oldData = consumable.toJSON();
const updateData = { ...req.body };
- if (Array.isArray(updateData.snList)) {
- updateData.currentStock = updateData.snList.length;
- }
+ // 禁止通过编辑接口修改库存和SN列表
+ delete updateData.currentStock;
+ delete updateData.snList;
await consumable.update(updateData, { transaction });
await ConsumableLog.create(
diff --git a/frontend/src/pages/ConsumableManagement.jsx b/frontend/src/pages/ConsumableManagement.jsx
index 3242ba3..1ddac7b 100644
--- a/frontend/src/pages/ConsumableManagement.jsx
+++ b/frontend/src/pages/ConsumableManagement.jsx
@@ -58,6 +58,7 @@ import {
QrcodeOutlined,
DesktopOutlined,
HistoryOutlined,
+ EyeOutlined,
} from '@ant-design/icons';
import axios from 'axios';
import * as XLSX from 'xlsx';
@@ -142,6 +143,13 @@ function ConsumableManagement() {
const [snInputValue, setSnInputValue] = useState('');
const [selectedSnList, setSelectedSnList] = useState([]);
const [snSearchKeyword, setSnSearchKeyword] = useState('');
+ const [createWithInbound, setCreateWithInbound] = useState(false);
+ const [inboundData, setInboundData] = useState({
+ quantity: 1,
+ operator: '',
+ reason: '',
+ });
+ const [inboundSnList, setInboundSnList] = useState([]);
const [scanModalVisible, setScanModalVisible] = useState(false);
const [scanMode, setScanMode] = useState('add');
const [scanValue, setScanValue] = useState('');
@@ -243,10 +251,12 @@ function ConsumableManagement() {
} else {
setMaxStockUnlimited(true);
setSnList([]);
+ setCreateWithInbound(false);
+ setInboundData({ quantity: 1, operator: '', reason: '' });
+ setInboundSnList([]);
form.resetFields();
form.setFieldsValue({
unit: '个',
- currentStock: 0,
minStock: 0,
status: 'active',
unitPrice: 0,
@@ -263,6 +273,8 @@ function ConsumableManagement() {
setSnList([]);
setSnInputVisible(false);
setSnInputValue('');
+ setCreateWithInbound(false);
+ setInboundData({ quantity: 1, operator: '', reason: '' });
}, []);
const handleSubmit = useCallback(
@@ -272,7 +284,6 @@ function ConsumableManagement() {
...values,
maxStock: maxStockUnlimited ? 0 : values.maxStock,
unitPrice: values.unitPrice || 0,
- snList: snList,
};
if (editingConsumable) {
await axios.put(`/api/consumables/${editingConsumable.consumableId}`, submitData);
@@ -281,25 +292,45 @@ function ConsumableManagement() {
icon: