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}

+

是否为该耗材入库?

+
+ ), + okText: '入库', + cancelText: '关闭', + onOk: () => { + handleScanCancel(); + setModalVisible(false); + showStockModal(existingConsumable, 'in'); + setSelectedSnList([code]); + stockForm.setFieldsValue({ quantity: 1 }); + }, + onCancel: () => { + setScanValue(''); + scanInputRef.current?.focus(); + } + }); + } else { + if (!snList.includes(code)) { + setSnList(prev => [...prev, code]); + const currentStock = form.getFieldValue('currentStock') || 0; + form.setFieldsValue({ currentStock: currentStock + 1 }); + message.success(`已添加SN: ${code}`); + } else { + message.warning(`SN已在列表中: ${code}`); + } + setScanValue(''); + scanInputRef.current?.focus(); + } + } catch (error) { + message.error('查询SN失败'); + console.error('查询SN失败:', error); + } finally { + setScanChecking(false); + } + } else if (scanMode === 'in') { + if (!scannedSnList.includes(code)) { + setScannedSnList(prev => [...prev, code]); + message.success(`已添加SN: ${code}`); + } else { + message.warning(`SN已存在: ${code}`); + } + setScanValue(''); + scanInputRef.current?.focus(); + } else if (scanMode === 'out') { + setScanChecking(true); + try { + const res = await axios.get(`/api/consumables/by-sn/${encodeURIComponent(code)}`); + if (res.data.found) { + handleScanCancel(); + showStockModal(res.data.consumable, 'out'); + setSelectedSnList([code]); + stockForm.setFieldsValue({ quantity: 1 }); + } else { + message.warning('未找到该SN对应的耗材'); + setScanValue(''); + scanInputRef.current?.focus(); + } + } catch (error) { + message.error('查询SN失败'); + console.error('查询SN失败:', error); + } finally { + setScanChecking(false); + } + } + } + }, [scanMode, scanValue, scannedSnList, handleScanCancel, showModal, form, showStockModal, stockForm]); + + const handleScanInSubmit = useCallback(async (consumableId) => { + if (scannedSnList.length === 0) { + message.warning('请先扫描SN序列号'); + return; + } + try { + await axios.post('/api/consumables/quick-inout', { + consumableId, + type: 'in', + quantity: scannedSnList.length, + operator: '系统管理员', + reason: '扫码入库', + snList: scannedSnList, + }); + message.success({ + content: `成功入库 ${scannedSnList.length} 个SN`, + icon: , + }); + handleScanCancel(); + fetchConsumables(); + } catch (error) { + message.error(error.response?.data?.error || '入库操作失败'); + console.error('入库失败:', error); + } + }, [scannedSnList, handleScanCancel, fetchConsumables]); + const columns = useMemo( () => [ { @@ -976,6 +1116,22 @@ function ConsumableManagement() { > 添加耗材 + +