From ad7c45d45336122fa599ef57b5605ebba3983c97 Mon Sep 17 00:00:00 2001 From: zhang1106 <849185023@qq.com> Date: Fri, 6 Mar 2026 15:41:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E4=B8=BB=E9=A2=98):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E4=B8=BB=E9=A2=98=E9=A2=9C=E8=89=B2=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/routes/inventory.js | 94 +++++ frontend/src/context/ConfigContext.jsx | 30 +- frontend/src/index.css | 16 +- frontend/src/pages/InventoryTaskExecution.jsx | 221 +++++++++++ frontend/src/pages/SystemSettings.jsx | 347 +++++++++++++++++- 5 files changed, 695 insertions(+), 13 deletions(-) diff --git a/backend/routes/inventory.js b/backend/routes/inventory.js index 9f8247d..d09ea56 100644 --- a/backend/routes/inventory.js +++ b/backend/routes/inventory.js @@ -520,4 +520,98 @@ router.get('/stats/dashboard', async (req, res) => { } }); +router.post('/quick-add-device', async (req, res) => { + try { + const { taskId, planId, serialNumber, deviceName, deviceType, rackId, position, remark } = req.body; + + if (!taskId || !planId || !serialNumber) { + return res.status(400).json({ error: '缺少必要参数:taskId, planId, serialNumber' }); + } + + const task = await InventoryTask.findByPk(taskId); + if (!task) { + return res.status(404).json({ error: '盘点任务不存在' }); + } + + const plan = await InventoryPlan.findByPk(planId); + if (!plan) { + return res.status(404).json({ error: '盘点计划不存在' }); + } + + const existingDevice = await Device.findOne({ where: { serialNumber } }); + if (existingDevice) { + return res.status(400).json({ error: '该序列号的设备已存在', deviceId: existingDevice.deviceId }); + } + + let deviceId; + const devices = await Device.findAll({ + where: { deviceId: { [Op.like]: 'DEV%' } } + }); + let maxNumber = 0; + devices.forEach(device => { + const match = device.deviceId.match(/^DEV(\d+)$/); + if (match) { + const num = parseInt(match[1], 10); + if (num > maxNumber) maxNumber = num; + } + }); + deviceId = `DEV${String(maxNumber + 1).padStart(3, '0')}`; + + const newDevice = await Device.create({ + deviceId, + name: deviceName || `新设备-${serialNumber.slice(-6)}`, + type: deviceType || 'other', + serialNumber, + rackId: rackId || null, + position: position || null, + status: 'running' + }); + + const record = await InventoryRecord.create({ + recordId: generateRecordId(), + taskId, + planId, + deviceId: newDevice.deviceId, + deviceName: newDevice.name, + deviceType: newDevice.type, + serialNumber: newDevice.serialNumber, + rackId: newDevice.rackId, + position: newDevice.position, + status: 'normal', + abnormalType: 'extra_device', + checkedBy: req.user?.userId, + checkedAt: new Date(), + remark: remark || '盘点时新增设备' + }); + + const taskRecords = await InventoryRecord.findAll({ where: { taskId: task.taskId } }); + const taskStats = { + totalDevices: taskRecords.length, + checkedDevices: taskRecords.filter(r => r.status !== 'pending').length, + normalDevices: taskRecords.filter(r => r.status === 'normal').length, + abnormalDevices: taskRecords.filter(r => r.status === 'abnormal').length + }; + await task.update(taskStats); + + const planRecords = await InventoryRecord.findAll({ where: { planId: plan.planId } }); + const planStats = { + totalDevices: planRecords.length, + checkedDevices: planRecords.filter(r => r.status !== 'pending').length, + normalDevices: planRecords.filter(r => r.status === 'normal').length, + abnormalDevices: planRecords.filter(r => r.status === 'abnormal').length, + missedDevices: planRecords.filter(r => r.status === 'pending').length + }; + await plan.update(planStats); + + res.status(201).json({ + message: '设备添加成功', + device: newDevice, + record + }); + } catch (error) { + console.error('快速添加设备错误:', error); + res.status(500).json({ error: error.message }); + } +}); + module.exports = router; diff --git a/frontend/src/context/ConfigContext.jsx b/frontend/src/context/ConfigContext.jsx index 4f72ae0..13ec7c2 100644 --- a/frontend/src/context/ConfigContext.jsx +++ b/frontend/src/context/ConfigContext.jsx @@ -3,9 +3,21 @@ import axios from 'axios'; const ConfigContext = createContext(); +const applyThemeColors = (primaryColor, secondaryColor) => { + const root = document.documentElement; + if (primaryColor) { + root.style.setProperty('--primary-color', primaryColor); + root.style.setProperty('--primary-light', `${primaryColor}20`); + root.style.setProperty('--primary-gradient', `linear-gradient(135deg, ${primaryColor} 0%, ${secondaryColor || '#764ba2'} 100%)`); + } + if (secondaryColor) { + root.style.setProperty('--secondary-color', secondaryColor); + root.style.setProperty('--secondary-light', `${secondaryColor}20`); + } +}; + export const ConfigProvider = ({ children }) => { const [config, setConfig] = useState({ - // 默认配置 site_name: '机柜管理系统', primary_color: '#667eea', secondary_color: '#764ba2', @@ -21,14 +33,12 @@ export const ConfigProvider = ({ children }) => { }); const [loading, setLoading] = useState(true); - // 加载系统配置 const loadConfig = async () => { try { const response = await axios.get('/api/system-settings'); const settings = response.data; const configValues = {}; - // 将配置转换为扁平结构 Object.entries(settings).forEach(([key, value]) => { configValues[key] = value.value; }); @@ -37,6 +47,10 @@ export const ConfigProvider = ({ children }) => { ...prev, ...configValues, })); + + if (configValues.primary_color || configValues.secondary_color) { + applyThemeColors(configValues.primary_color, configValues.secondary_color); + } } catch (error) { console.error('加载系统配置失败:', error); } finally { @@ -44,12 +58,16 @@ export const ConfigProvider = ({ children }) => { } }; - // 初始化加载配置 useEffect(() => { loadConfig(); }, []); - // 更新配置 + useEffect(() => { + if (config.primary_color || config.secondary_color) { + applyThemeColors(config.primary_color, config.secondary_color); + } + }, [config.primary_color, config.secondary_color]); + const updateConfig = newConfig => { setConfig(prev => ({ ...prev, @@ -57,7 +75,6 @@ export const ConfigProvider = ({ children }) => { })); }; - // 重新加载配置 const reloadConfig = async () => { await loadConfig(); }; @@ -69,7 +86,6 @@ export const ConfigProvider = ({ children }) => { ); }; -// 自定义钩子,方便组件使用配置 export const useConfig = () => { const context = useContext(ConfigContext); if (!context) { diff --git a/frontend/src/index.css b/frontend/src/index.css index 5ed86f9..869e96a 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,3 +1,11 @@ +:root { + --primary-color: #667eea; + --primary-light: rgba(102, 126, 234, 0.125); + --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + --secondary-color: #764ba2; + --secondary-light: rgba(118, 75, 162, 0.125); +} + * { margin: 0; padding: 0; @@ -349,19 +357,19 @@ body { } .ant-btn-primary { - background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%) !important; + background: var(--primary-gradient) !important; border: none !important; box-shadow: 0 2px 6px rgba(24, 144, 255, 0.35) !important; } .ant-btn-primary:hover { - background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%) !important; + background: var(--primary-gradient) !important; box-shadow: 0 4px 12px rgba(24, 144, 255, 0.45) !important; transform: translateY(-2px) !important; } .ant-btn-primary:active { - background: linear-gradient(135deg, #096dd9 0%, #0050b3 100%) !important; + background: var(--primary-gradient) !important; box-shadow: 0 2px 6px rgba(24, 144, 255, 0.35) !important; transform: translateY(0) !important; } @@ -871,7 +879,7 @@ body { } .welcome-banner { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: var(--primary-gradient); border-radius: 12px; padding: 24px; color: #fff; diff --git a/frontend/src/pages/InventoryTaskExecution.jsx b/frontend/src/pages/InventoryTaskExecution.jsx index dce2b24..e88612c 100644 --- a/frontend/src/pages/InventoryTaskExecution.jsx +++ b/frontend/src/pages/InventoryTaskExecution.jsx @@ -30,6 +30,7 @@ import { ScanOutlined, CheckOutlined, CloseOutlined, + PlusOutlined, } from '@ant-design/icons'; import axios from 'axios'; import dayjs from 'dayjs'; @@ -67,6 +68,12 @@ const InventoryTaskExecution = () => { const [scanResult, setScanResult] = useState(null); const [scanModalVisible, setScanModalVisible] = useState(false); const [recordPagination, setRecordPagination] = useState({ current: 1, pageSize: 20, total: 0 }); + const [quickAddModalVisible, setQuickAddModalVisible] = useState(false); + const [quickAddForm] = Form.useForm(); + const [quickAddLoading, setQuickAddLoading] = useState(false); + const [rooms, setRooms] = useState([]); + const [racks, setRacks] = useState([]); + const [selectedRoomId, setSelectedRoomId] = useState(null); const planId = searchParams.get('planId'); @@ -245,6 +252,84 @@ const InventoryTaskExecution = () => { fetchPlan(); }, [fetchPlan]); + const fetchRooms = async () => { + try { + const res = await api.get('/rooms'); + setRooms(res.data.rooms || res.data || []); + } catch (error) { + console.error('获取机房列表失败', error); + } + }; + + const fetchRacks = async () => { + try { + const res = await api.get('/racks'); + setRacks(res.data.racks || res.data || []); + } catch (error) { + console.error('获取机柜列表失败', error); + } + }; + + useEffect(() => { + fetchRooms(); + fetchRacks(); + }, []); + + const filteredRacks = selectedRoomId + ? racks.filter(rack => rack.roomId === selectedRoomId) + : racks; + + const handleQuickAddDevice = async (values) => { + if (!currentTask || !plan) { + message.error('请先选择盘点任务'); + return; + } + setQuickAddLoading(true); + try { + const res = await api.post('/inventory/quick-add-device', { + taskId: currentTask.taskId, + planId: plan.planId, + serialNumber: scanResult?.sn || values.serialNumber, + deviceName: values.deviceName, + deviceType: values.deviceType, + rackId: values.rackId, + position: values.position, + remark: values.remark + }); + message.success('设备添加成功!'); + setQuickAddModalVisible(false); + quickAddForm.resetFields(); + setScanResult({ + success: true, + message: `新设备 "${res.data.device.name}" 已添加并完成盘点!`, + record: res.data.record, + sn: res.data.device.serialNumber + }); + if (currentTask) { + fetchTaskRecords(currentTask.taskId); + } + fetchPlan(); + } catch (error) { + message.error(error.response?.data?.error || '添加设备失败'); + } finally { + setQuickAddLoading(false); + } + }; + + const openQuickAddModal = () => { + setSelectedRoomId(null); + quickAddForm.setFieldsValue({ + serialNumber: scanResult?.sn || '', + deviceName: '', + deviceType: 'other', + roomId: undefined, + rackId: undefined, + position: undefined, + remark: undefined, + }); + setQuickAddModalVisible(true); + }; + const handleCheck = (record) => { setCurrentRecord(record); form.setFieldsValue({ @@ -912,6 +997,18 @@ const InventoryTaskExecution = () => { )} + {!scanResult.success && scanResult.sn && ( +
+ +
+ )} )} @@ -1037,6 +1134,130 @@ const InventoryTaskExecution = () => { )} + + + + 快速添加设备 + + } + open={quickAddModalVisible} + onCancel={() => { + setQuickAddModalVisible(false); + setSelectedRoomId(null); + }} + footer={null} + width={500} + centered + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
); }; diff --git a/frontend/src/pages/SystemSettings.jsx b/frontend/src/pages/SystemSettings.jsx index 7cc708c..f5c6df5 100644 --- a/frontend/src/pages/SystemSettings.jsx +++ b/frontend/src/pages/SystemSettings.jsx @@ -19,6 +19,7 @@ import { Badge, Tooltip, Avatar, + ColorPicker, } from 'antd'; import { SettingOutlined, @@ -668,18 +669,360 @@ const SystemSettings = () => { ); }; + const presetColors = [ + '#667eea', + '#764ba2', + '#f093fb', + '#4facfe', + '#43e97b', + '#fa709a', + '#fee140', + '#00b4db', + '#0083b0', + '#fcb045', + '#1890ff', + '#52c41a', + '#eb2f96', + '#722ed1', + '#13c2c2', + '#fa8c16', + ]; + + const handleColorChange = (color, key) => { + const hexColor = typeof color === 'string' ? color : color.toHexString(); + form.setFieldValue(key, hexColor); + + const root = document.documentElement; + if (key === 'primary_color') { + root.style.setProperty('--primary-color', hexColor); + root.style.setProperty('--primary-light', `${hexColor}20`); + } else if (key === 'secondary_color') { + root.style.setProperty('--secondary-color', hexColor); + root.style.setProperty('--secondary-light', `${hexColor}20`); + } + }; + + const renderColorFormItem = (key, data) => { + const currentValue = form.getFieldValue(key) || settings[key]?.value || '#667eea'; + + return ( + + + {data.description || key} + +
+ + } + name={key} + style={{ marginBottom: 0 }} + > + + + ); + }; + const renderAppearanceSettings = () => { + const primaryColor = form.getFieldValue('primary_color') || settings.primary_color?.value || '#667eea'; + const secondaryColor = form.getFieldValue('secondary_color') || settings.secondary_color?.value || '#764ba2'; + return (
- {settingGroups.appearance.map(group => renderSettingGroup(group))} + + +
+
+ +
+
+ 主题颜色 +
+
+ 自定义系统主题配色方案 +
+
+ + + + + +
+ 主色调 + + 用于按钮、链接等主要交互元素 + +
+ + handleColorChange(color, 'primary_color')} + format="hex" + showText + presets={[ + { + label: '推荐配色', + colors: presetColors, + }, + ]} + style={{ width: '100%' }} + /> + handleColorChange(e.target.value, 'primary_color')} + placeholder="#667eea" + style={{ borderRadius: 8 }} + prefix={} + /> + + + +
+ 次色调 + + 用于渐变、悬停效果等辅助元素 + +
+ + handleColorChange(color, 'secondary_color')} + format="hex" + showText + presets={[ + { + label: '推荐配色', + colors: presetColors, + }, + ]} + style={{ width: '100%' }} + /> + handleColorChange(e.target.value, 'secondary_color')} + placeholder="#764ba2" + style={{ borderRadius: 8 }} + prefix={} + /> + + +
+ + + +
+ + 实时预览 + + + + + + 标签样式 + +
+ +
+ + + +
+
+ +
+
+ 界面布局 +
+
+ 调整界面显示密度和布局方式 +
+
+ + + + + + 紧凑模式} + name="compact_mode" + valuePropName="checked" + style={{ marginBottom: 0 }} + > + + + + + + + 侧边栏折叠} + name="sidebar_collapsed" + valuePropName="checked" + style={{ marginBottom: 0 }} + > + + + + + + + 表格行高度} + name="table_row_height" + style={{ marginBottom: 0 }} + > + + + + +
+ + +
+
+ +
+
+ 动画效果 +
+
+ 控制界面动画和过渡效果 +
+
+ + + + + + 启用动画} + name="animation_enabled" + valuePropName="checked" + style={{ marginBottom: 0 }} + > + + + + + + +
{renderFixedFooter()}