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: , }); } else { - await axios.post('/api/consumables', { - ...submitData, - consumableId: `CON${Date.now()}`, - }); - message.success({ - content: '耗材创建成功', - icon: , - }); + if (createWithInbound) { + // 同时入库模式,调用创建并入库接口 + await axios.post('/api/consumables/create-with-inbound', { + ...submitData, + consumableId: `CON${Date.now()}`, + inboundQuantity: inboundData.quantity, + inboundOperator: inboundData.operator, + inboundReason: inboundData.reason, + inboundSnList: inboundSnList, + }); + message.success({ + content: '耗材创建成功并已入库', + icon: , + }); + } else { + // 普通创建模式 + await axios.post('/api/consumables', { + ...submitData, + consumableId: `CON${Date.now()}`, + }); + message.success({ + content: '耗材创建成功', + icon: , + }); + } } setModalVisible(false); fetchConsumables(); setEditingConsumable(null); setSnList([]); + setCreateWithInbound(false); + setInboundData({ quantity: 1, operator: '', reason: '' }); + setInboundSnList([]); } catch (error) { message.error(editingConsumable ? '耗材更新失败' : '耗材创建失败'); console.error('提交失败:', error); } }, - [editingConsumable, fetchConsumables, maxStockUnlimited, snList] + [editingConsumable, fetchConsumables, maxStockUnlimited, createWithInbound, inboundData, inboundSnList] ); const handleDelete = useCallback( @@ -1030,10 +1061,15 @@ function ConsumableManagement() { }, }); } else { - if (!snList.includes(code)) { - setSnList(prev => [...prev, code]); - const currentStock = form.getFieldValue('currentStock') || 0; - form.setFieldsValue({ currentStock: currentStock + 1 }); + // 区分添加到哪个列表 + const targetList = createWithInbound ? inboundSnList : snList; + const setTargetList = createWithInbound ? setInboundSnList : setSnList; + if (!targetList.includes(code)) { + setTargetList(prev => [...prev, code]); + // 如果是同时入库模式,自动同步入库数量 + if (createWithInbound) { + setInboundData(prev => ({ ...prev, quantity: targetList.length + 1 })); + } message.success(`已添加SN: ${code}`); } else { message.warning(`SN已在列表中: ${code}`); @@ -1111,6 +1147,12 @@ function ConsumableManagement() { scanMode, scanValue, scannedSnList, + snList, + setSnList, + createWithInbound, + inboundSnList, + setInboundSnList, + setInboundData, handleScanCancel, showModal, form, @@ -1974,106 +2016,466 @@ function ConsumableManagement() { - {/* 库存管理 */} -
+ {/* 库存预警设置 */} + {!editingConsumable && (
- 2 +
+ 2 +
+ 库存预警
- 库存管理 - - 设置库存预警和上限 - + + + + + + + + +
+ { + setMaxStockUnlimited(e.target.checked); + if (e.target.checked) { + form.setFieldsValue({ maxStock: undefined }); + } + }} + > + 无限制 + +
+ {!maxStockUnlimited && ( + + + + )} +
+ +
- - - - - - - - - - - - - -
- { - setMaxStockUnlimited(e.target.checked); - if (e.target.checked) { - form.setFieldsValue({ maxStock: undefined }); - } - }} - > - 无限制 - + )} + + {/* 初始入库(添加模式专用) */} + {!editingConsumable && ( +
+
+ setCreateWithInbound(e.target.checked)} + style={{ marginTop: '4px' }} + /> +
+
+ + 同时进行初始入库 + + + 推荐 +
- {!maxStockUnlimited && ( - + 创建耗材后立即录入初始库存数量,生成入库记录 + + + {createWithInbound && ( + - - +
+ + + + + setInboundData({ ...inboundData, quantity: value || 0 }) + } + style={{ ...inputNumberStyles.base, width: '100%' }} + placeholder="输入数量" + /> + + + + + + setInboundData({ ...inboundData, operator: e.target.value }) + } + placeholder="输入操作人" + style={inputStyles.form} + /> + + + + + + setInboundData({ ...inboundData, reason: e.target.value }) + } + placeholder="输入入库原因" + style={inputStyles.form} + /> + + + + + {/* SN序列号管理 */} +
+
+ + SN序列号(可选) + + + + + +
+ {inboundSnList.length > 0 ? ( +
+ {inboundSnList.map((sn, index) => ( + { + const newList = inboundSnList.filter((_, i) => i !== index); + setInboundSnList(newList); + setInboundData(prev => ({ ...prev, quantity: newList.length })); + }} + color="blue" + style={{ marginBottom: '4px', marginRight: '4px' }} + > + {sn} + + ))} +
+ ) : ( + + 暂未添加SN序列号 + + )} +
+
+ )} - - - -
+
+
+
+ )} + + {/* 编辑模式下的库存预警设置 */} + {editingConsumable && ( +
+
+
+ 2 +
+ 库存预警 +
+ + + + + + + + +
+ { + setMaxStockUnlimited(e.target.checked); + if (e.target.checked) { + form.setFieldsValue({ maxStock: undefined }); + } + }} + > + 无限制 + +
+ {!maxStockUnlimited && ( + + + + )} +
+ +
+
+ )} + + {/* 编辑模式下的当前库存信息展示 */} + {editingConsumable && ( +
+
+
+ +
+
+
+ + 当前库存信息 + + + 只读 + +
+ + 如需调整库存,请使用「调整库存」功能 + + +
+
+
+ {editingConsumable.currentStock || 0} +
+
+ 当前库存 ({editingConsumable.unit}) +
+
+
+
+ {editingConsumable.snList?.length || 0} +
+
+ 已录入SN +
+
+
+
+ {editingConsumable.currentStock <= editingConsumable.minStock ? '⚠️ 低' : '✓ 正常'} +
+
+ 库存状态 +
+
+
+
+
+
+ )} {/* 价格与状态 */}
- {/* SN序列号管理 */} -
-
-
-
- 5 -
- SN序列号管理 - - 已录入 {snList.length} 个 - -
- - - - - -
- - {/* 批量添加输入区 */} - {snInputVisible && ( - - -