From a316fa4d38cbd620a4cb64c835e113769d66bca8 Mon Sep 17 00:00:00 2001 From: zhang1106 <849185023@qq.com> Date: Tue, 10 Mar 2026 11:10:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=80=97=E6=9D=90=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=89=AB=E7=A0=81=E5=8A=9F=E8=83=BD=E5=8F=8A?= =?UTF-8?q?SN=E6=9F=A5=E8=AF=A2=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现耗材SN扫码功能,包括扫码添加、入库和出库操作 添加后端API接口根据SN查询耗材信息 优化耗材表单UI,分步骤展示不同信息区块 同步数据库模型确保耗材相关表结构正确 --- backend/routes/consumables.js | 17 + backend/scripts/init-database.js | 40 +- backend/server.js | 36 + frontend/src/api/cache.js | 1 + frontend/src/pages/ConsumableManagement.jsx | 950 ++++++++++++++++---- 5 files changed, 865 insertions(+), 179 deletions(-) diff --git a/backend/routes/consumables.js b/backend/routes/consumables.js index 9765cf6..c0d5cc7 100644 --- a/backend/routes/consumables.js +++ b/backend/routes/consumables.js @@ -179,6 +179,23 @@ router.post('/import', async (req, res) => { } }); +router.get('/by-sn/:sn', async (req, res) => { + try { + const sn = req.params.sn; + const consumables = await Consumable.findAll(); + const consumable = consumables.find(c => { + const snList = c.snList || []; + return snList.includes(sn); + }); + res.json({ + found: !!consumable, + consumable + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + router.get('/categories/list', async (req, res) => { try { const categories = await Consumable.findAll({ diff --git a/backend/scripts/init-database.js b/backend/scripts/init-database.js index 774fbfa..b2ec7e1 100644 --- a/backend/scripts/init-database.js +++ b/backend/scripts/init-database.js @@ -9,6 +9,19 @@ const { sequelize } = require('../db'); const SystemSetting = require('../models/SystemSetting'); +const Consumable = require('../models/Consumable'); +const ConsumableLog = require('../models/ConsumableLog'); +const ConsumableCategory = require('../models/ConsumableCategory'); +const ConsumableRecord = require('../models/ConsumableRecord'); +const ConsumableLogArchive = require('../models/ConsumableLogArchive'); +const Device = require('../models/Device'); +const Rack = require('../models/Rack'); +const Room = require('../models/Room'); +const User = require('../models/User'); +const FaultCategory = require('../models/FaultCategory'); +const InventoryPlan = require('../models/InventoryPlan'); +const InventoryTask = require('../models/InventoryTask'); +const InventoryRecord = require('../models/InventoryRecord'); // 默认系统设置(包含中文描述) const defaultSettings = [ @@ -50,8 +63,31 @@ async function initDatabase() { await sequelize.authenticate(); console.log('数据库连接成功'); - // 同步所有模型 - await sequelize.sync({ force: false, alter: true }); + // 同步所有模型(按依赖顺序) + console.log('开始同步模型...'); + + // 基础模型 + await User.sync({ alter: true }); + await Room.sync({ alter: true }); + await Rack.sync({ alter: true }); + await Device.sync({ alter: true }); + await FaultCategory.sync({ alter: true }); + + // 耗材相关模型 + await ConsumableCategory.sync({ alter: true }); + await Consumable.sync({ alter: true }); + await ConsumableLog.sync({ alter: true }); + await ConsumableRecord.sync({ alter: true }); + await ConsumableLogArchive.sync({ alter: true }); + + // 盘点相关模型 + await InventoryPlan.sync({ alter: true }); + await InventoryTask.sync({ alter: true }); + await InventoryRecord.sync({ alter: true }); + + // 系统设置 + await SystemSetting.sync({ alter: true }); + console.log('数据库表结构同步完成'); // 初始化或更新系统设置 diff --git a/backend/server.js b/backend/server.js index d2c19ff..c3423cd 100644 --- a/backend/server.js +++ b/backend/server.js @@ -43,6 +43,24 @@ sequelize.authenticate() const SystemSetting = require('./models/SystemSetting'); return SystemSetting.sync(); }) + .then(() => { + // 同步耗材模型 + const Consumable = require('./models/Consumable'); + const ConsumableLog = require('./models/ConsumableLog'); + const ConsumableCategory = require('./models/ConsumableCategory'); + const ConsumableRecord = require('./models/ConsumableRecord'); + const ConsumableLogArchive = require('./models/ConsumableLogArchive'); + return Promise.all([ + Consumable.sync({ alter: true }), + ConsumableLog.sync(), + ConsumableCategory.sync(), + ConsumableRecord.sync(), + ConsumableLogArchive.sync() + ]); + }) + .then(() => { + console.log('耗材模型同步完成'); + }) .then(() => { // 同步盘点模型 const InventoryPlan = require('./models/InventoryPlan'); @@ -57,6 +75,24 @@ sequelize.authenticate() .then(() => { console.log('盘点模型同步完成'); }) + .then(() => { + // 同步耗材相关模型 + const Consumable = require('./models/Consumable'); + const ConsumableLog = require('./models/ConsumableLog'); + const ConsumableCategory = require('./models/ConsumableCategory'); + const ConsumableRecord = require('./models/ConsumableRecord'); + const ConsumableLogArchive = require('./models/ConsumableLogArchive'); + return Promise.all([ + Consumable.sync({ alter: true }), + ConsumableLog.sync(), + ConsumableCategory.sync(), + ConsumableRecord.sync(), + ConsumableLogArchive.sync() + ]); + }) + .then(() => { + console.log('耗材模型同步完成'); + }) .then(() => { // 初始化系统设置默认值(关键:确保部署时数据正确初始化) console.log('开始初始化系统设置默认值...'); diff --git a/frontend/src/api/cache.js b/frontend/src/api/cache.js index 8d51dd5..41c5101 100644 --- a/frontend/src/api/cache.js +++ b/frontend/src/api/cache.js @@ -253,6 +253,7 @@ export const consumableAPI = { quickInOut: data => cachedAPI.post('/consumables/quick-inout', data), getStatistics: () => cachedAPI.get('/consumables/statistics/summary'), getLowStock: () => cachedAPI.get('/consumables/low-stock'), + getBySn: sn => cachedAPI.get(`/consumables/by-sn/${encodeURIComponent(sn)}`), }; export const consumableCategoryAPI = { diff --git a/frontend/src/pages/ConsumableManagement.jsx b/frontend/src/pages/ConsumableManagement.jsx index d423f83..f6fd0a9 100644 --- a/frontend/src/pages/ConsumableManagement.jsx +++ b/frontend/src/pages/ConsumableManagement.jsx @@ -43,6 +43,8 @@ import { ExclamationCircleOutlined, ArrowUpOutlined, ArrowDownOutlined, + ScanOutlined, + BarcodeOutlined, } from '@ant-design/icons'; import axios from 'axios'; import { motion, AnimatePresence } from 'framer-motion'; @@ -181,6 +183,13 @@ function ConsumableManagement() { const [snInputValue, setSnInputValue] = useState(''); const [selectedSnList, setSelectedSnList] = useState([]); const [snSearchKeyword, setSnSearchKeyword] = useState(''); + const [scanModalVisible, setScanModalVisible] = useState(false); + const [scanMode, setScanMode] = useState('add'); + const [scanValue, setScanValue] = useState(''); + const [scanChecking, setScanChecking] = useState(false); + const [scannedSnList, setScannedSnList] = useState([]); + const [selectedScanInConsumable, setSelectedScanInConsumable] = useState(null); + const scanInputRef = React.useRef(null); const fetchConsumables = useCallback( async (page = 1, pageSize = 10) => { @@ -611,6 +620,137 @@ function ConsumableManagement() { [stockRecord, stockType, fetchConsumables, selectedSnList] ); + const showScanModal = useCallback((mode) => { + setScanMode(mode); + setScanValue(''); + setScannedSnList([]); + setSelectedScanInConsumable(null); + setScanModalVisible(true); + setTimeout(() => { + scanInputRef.current?.focus(); + }, 100); + }, []); + + const handleScanCancel = useCallback(() => { + setScanModalVisible(false); + setScanValue(''); + setScannedSnList([]); + setSelectedScanInConsumable(null); + setScanChecking(false); + }, []); + + const handleScanKeyDown = useCallback(async (e) => { + if (e.key === 'Enter' && scanValue.trim()) { + e.preventDefault(); + const code = scanValue.trim(); + + if (scanMode === 'add') { + setScanChecking(true); + try { + const res = await axios.get(`/api/consumables/by-sn/${encodeURIComponent(code)}`); + if (res.data.found) { + const existingConsumable = res.data.consumable; + Modal.confirm({ + title: 'SN已存在', + content: ( +
该SN已关联耗材:{existingConsumable.name}
+分类:{existingConsumable.category}
+当前库存:{existingConsumable.currentStock} {existingConsumable.unit}
+是否为该耗材入库?
+