From e363553e90c85b4415d4f4038e98d9b6823c3cac Mon Sep 17 00:00:00 2001 From: zhang1106 <849185023@qq.com> Date: Fri, 19 Dec 2025 13:21:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BA=86=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E7=9A=84=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=B8=80?= =?UTF-8?q?=E9=94=AE=E5=88=A0=E9=99=A4=EF=BC=8C=E4=BF=AE=E5=A4=8D=E4=BA=86?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=AD=97=E6=AE=B5=E7=9A=84bug,=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=BA=86=E5=AF=BC=E5=85=A5=E8=AE=BE=E5=A4=87=E7=9A=84?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/routes/devices.js | 119 +++++---- frontend/src/pages/DeviceManagement.jsx | 306 ++++++++++++++---------- 2 files changed, 265 insertions(+), 160 deletions(-) diff --git a/backend/routes/devices.js b/backend/routes/devices.js index 08ac8ec..0a58855 100644 --- a/backend/routes/devices.js +++ b/backend/routes/devices.js @@ -401,47 +401,86 @@ router.post('/import', async (req, res) => { }); try { - // 验证必填字段 - if (!fieldValueMap['设备ID'] || !fieldValueMap['设备名称'] || !fieldValueMap['设备类型'] || !fieldValueMap['型号'] || !fieldValueMap['序列号'] || !fieldValueMap['所在机柜ID'] || !fieldValueMap['位置(U)'] || !fieldValueMap['高度(U)'] || !fieldValueMap['功率(W)'] || !fieldValueMap['状态'] || !fieldValueMap['购买日期'] || !fieldValueMap['保修到期']) { - throw new Error('缺少必填字段'); - } - - // 验证设备ID是否已存在 - const existingDevice = await Device.findByPk(fieldValueMap['设备ID']); - if (existingDevice) { - throw new Error('设备ID已存在'); - } - - // 验证机柜是否存在,如果不存在则自动创建 - if (!rackIds.includes(fieldValueMap['所在机柜ID'])) { - // 自动创建机柜 - const newRack = await Rack.create({ - rackId: fieldValueMap['所在机柜ID'], - name: fieldValueMap['所在机柜ID'], // 使用机柜ID作为默认名称 - height: 42, // 默认42U高度 - maxPower: 10000, // 默认最大功耗10000W - currentPower: 0, - status: 'active', - roomId: defaultRoomId - }); + // 验证必填字段 + const requiredFields = { + '设备ID': fieldValueMap['设备ID'], + '设备名称': fieldValueMap['设备名称'], + '设备类型': fieldValueMap['设备类型'], + '型号': fieldValueMap['型号'], + '序列号': fieldValueMap['序列号'], + '所在机柜ID': fieldValueMap['所在机柜ID'], + '位置(U)': fieldValueMap['位置(U)'], + '高度(U)': fieldValueMap['高度(U)'], + '功率(W)': fieldValueMap['功率(W)'], + '状态': fieldValueMap['状态'], + '购买日期': fieldValueMap['购买日期'], + '保修到期': fieldValueMap['保修到期'] + }; - // 更新机柜列表和ID列表 - racks.push(newRack); - rackIds.push(newRack.rackId); - } - - // 验证状态值 - const validStatus = ['running', 'maintenance', 'offline', 'fault']; - if (!validStatus.includes(fieldValueMap['状态'])) { - throw new Error('状态值无效,有效值为:running, maintenance, offline, fault'); - } - - // 验证日期格式并解析日期 - const purchaseDate = new Date(fieldValueMap['购买日期']); - const warrantyExpiry = new Date(fieldValueMap['保修到期']); - if (isNaN(purchaseDate.getTime()) || isNaN(warrantyExpiry.getTime())) { - throw new Error('日期格式无效,请使用有效的日期格式'); - } + const missingFields = Object.keys(requiredFields).filter(fieldName => !requiredFields[fieldName]); + if (missingFields.length > 0) { + throw new Error(`缺少必填字段:${missingFields.join('、')}`); + } + + // 验证序列号是否已存在 + const existingDevice = await Device.findOne({ + where: { serialNumber: fieldValueMap['序列号'] } + }); + if (existingDevice) { + throw new Error(`序列号已存在:${fieldValueMap['序列号']}`); + } + + // 验证机柜是否存在,如果不存在则自动创建 + if (!rackIds.includes(fieldValueMap['所在机柜ID'])) { + // 自动创建机柜 + const newRack = await Rack.create({ + rackId: fieldValueMap['所在机柜ID'], + name: fieldValueMap['所在机柜ID'], // 使用机柜ID作为默认名称 + height: 42, // 默认42U高度 + maxPower: 10000, // 默认最大功耗10000W + currentPower: 0, + status: 'active', + roomId: defaultRoomId + }); + + // 更新机柜列表和ID列表 + racks.push(newRack); + rackIds.push(newRack.rackId); + } + + // 验证状态值 + const validStatus = ['running', 'maintenance', 'offline', 'fault']; + if (!validStatus.includes(fieldValueMap['状态'])) { + throw new Error(`状态值无效,有效值为:${validStatus.join('、')},当前值:${fieldValueMap['状态']}`); + } + + // 验证数字字段 + const numericFields = { + '位置(U)': fieldValueMap['位置(U)'], + '高度(U)': fieldValueMap['高度(U)'], + '功率(W)': fieldValueMap['功率(W)'] + }; + + for (const [fieldName, value] of Object.entries(numericFields)) { + if (isNaN(Number(value))) { + throw new Error(`${fieldName}必须是数字,当前值:${value}`); + } + } + + // 验证日期格式并解析日期 + const purchaseDate = new Date(fieldValueMap['购买日期']); + const warrantyExpiry = new Date(fieldValueMap['保修到期']); + if (isNaN(purchaseDate.getTime())) { + throw new Error(`购买日期格式无效:${fieldValueMap['购买日期']},请使用YYYY-MM-DD格式`); + } + if (isNaN(warrantyExpiry.getTime())) { + throw new Error(`保修到期日期格式无效:${fieldValueMap['保修到期']},请使用YYYY-MM-DD格式`); + } + + // 验证日期逻辑 + if (warrantyExpiry <= purchaseDate) { + throw new Error(`保修到期日期必须晚于购买日期:购买日期${fieldValueMap['购买日期']},保修到期${fieldValueMap['保修到期']}`); + } // 构建设备数据 const deviceData = { diff --git a/frontend/src/pages/DeviceManagement.jsx b/frontend/src/pages/DeviceManagement.jsx index 9072131..6ac08ed 100644 --- a/frontend/src/pages/DeviceManagement.jsx +++ b/frontend/src/pages/DeviceManagement.jsx @@ -28,9 +28,7 @@ function DeviceManagement() { showSizeChanger: true, showTotal: (total) => `共 ${total} 条记录` }); - // 自定义字段状态 - const [customFieldName, setCustomFieldName] = useState(''); - const [customFieldValue, setCustomFieldValue] = useState(''); + // 设备字段配置 const [deviceFields, setDeviceFields] = useState([]); const [loadingFields, setLoadingFields] = useState(true); @@ -41,6 +39,10 @@ function DeviceManagement() { const [importResult, setImportResult] = useState(null); const [selectedDevices, setSelectedDevices] = useState([]); const [selectAll, setSelectAll] = useState(false); + // 自定义字段状态 + const [customFieldName, setCustomFieldName] = useState(''); + const [customFieldValue, setCustomFieldValue] = useState(''); + // 字段配置模态框 const [fieldConfigModalVisible, setFieldConfigModalVisible] = useState(false); @@ -149,14 +151,47 @@ function DeviceManagement() { const deviceData = { ...device }; if (deviceData.purchaseDate) deviceData.purchaseDate = dayjs(deviceData.purchaseDate); if (deviceData.warrantyExpiry) deviceData.warrantyExpiry = dayjs(deviceData.warrantyExpiry); - if (!deviceData.customFields) deviceData.customFields = {}; - // 将customFields中的字段值合并到deviceData中,以便动态表单控件能正确显示 - Object.entries(deviceData.customFields).forEach(([fieldName, value]) => { - deviceData[fieldName] = value; + // 定义设备模型的固定字段 + const fixedFields = [ + 'deviceId', 'name', 'type', 'model', 'serialNumber', 'rackId', + 'position', 'height', 'powerConsumption', 'status', 'purchaseDate', + 'warrantyExpiry', 'ipAddress', 'description' + ]; + + // 定义需要排除的系统字段 + const systemFields = ['createdAt', 'updatedAt', 'Rack', 'Room', 'customFields']; + + // 创建一个干净的设备数据对象 + const cleanDeviceData = {}; + + // 复制固定字段 + fixedFields.forEach(field => { + if (deviceData[field] !== undefined) { + cleanDeviceData[field] = deviceData[field]; + } }); - form.setFieldsValue(deviceData); + // 将自定义字段直接添加到表单数据中(不在customFields对象内) + Object.entries(deviceData).forEach(([key, value]) => { + // 排除固定字段、系统字段和非基本类型的值 + if (!fixedFields.includes(key) && + !systemFields.includes(key) && + key !== 'deviceId' && + typeof value !== 'object' && + value !== null) { + cleanDeviceData[key] = value; + } + }); + + // 如果有原始customFields对象,将其字段也添加到表单数据中 + if (deviceData.customFields && typeof deviceData.customFields === 'object') { + Object.entries(deviceData.customFields).forEach(([key, value]) => { + cleanDeviceData[key] = value; + }); + } + + form.setFieldsValue(cleanDeviceData); } else { form.resetFields(); } @@ -179,31 +214,22 @@ function DeviceManagement() { 'warrantyExpiry', 'ipAddress', 'description' ]; - // 分离固定字段和动态字段 - const fixedFieldValues = {}; - const dynamicFieldValues = {}; + // 构建最终的设备数据,包含固定字段和自定义字段 + const deviceData = { + ...values, + purchaseDate: values.purchaseDate ? values.purchaseDate.format('YYYY-MM-DD') : null, + warrantyExpiry: values.warrantyExpiry ? values.warrantyExpiry.format('YYYY-MM-DD') : null, + customFields: {} // 用于存储自定义字段 + }; - Object.entries(values).forEach(([key, value]) => { - if (fixedFields.includes(key)) { - fixedFieldValues[key] = value; - } else if (key !== 'customFields') { - dynamicFieldValues[key] = value; + // 分离固定字段和自定义字段 + Object.keys(deviceData).forEach(key => { + if (!fixedFields.includes(key) && key !== 'customFields') { + // 将非固定字段移动到customFields对象中 + deviceData.customFields[key] = deviceData[key]; + delete deviceData[key]; } }); - - // 合并原有的自定义字段和新的动态字段 - const allCustomFields = { - ...(values.customFields || {}), - ...dynamicFieldValues - }; - - // 构建最终的设备数据 - const deviceData = { - ...fixedFieldValues, - purchaseDate: fixedFieldValues.purchaseDate ? fixedFieldValues.purchaseDate.format('YYYY-MM-DD') : null, - warrantyExpiry: fixedFieldValues.warrantyExpiry ? fixedFieldValues.warrantyExpiry.format('YYYY-MM-DD') : null, - customFields: allCustomFields - }; if (editingDevice) { // 更新设备 @@ -397,7 +423,14 @@ function DeviceManagement() { setImportProgress(100); setImportResult(response.data); setIsImporting(false); - message.success('导入完成'); + + // 根据导入结果显示不同的消息 + const { success, failed } = response.data.statistics; + if (failed > 0) { + message.warning(`导入完成,但有 ${failed} 条记录导入失败`); + } else { + message.success('所有记录导入成功'); + } // 重新加载设备列表 setTimeout(() => { @@ -461,7 +494,18 @@ function DeviceManagement() { title: field.displayName, dataIndex: field.fieldName, key: field.fieldName, - render: (type) => typeMap[type], + render: (type) => { + if (Array.isArray(type)) { + return ( + + {type.map(t => ( + {typeMap[t]} + ))} + + ); + } + return typeMap[type]; + }, }); } // 特殊处理状态字段 @@ -470,11 +514,24 @@ function DeviceManagement() { title: field.displayName, dataIndex: field.fieldName, key: field.fieldName, - render: (status) => ( - - {statusMap[status]?.text || status} - - ), + render: (status) => { + if (Array.isArray(status)) { + return ( + + {status.map(s => ( + + {statusMap[s]?.text || s} + + ))} + + ); + } + return ( + + {statusMap[status]?.text || status} + + ); + }, }); } // 特殊处理日期字段 @@ -483,7 +540,27 @@ function DeviceManagement() { title: field.displayName, dataIndex: field.fieldName, key: field.fieldName, - render: (date) => date ? new Date(date).toLocaleDateString('zh-CN') : '', + render: (date) => { + if (!date) return ''; + + const dateObj = new Date(date); + const formattedDate = dateObj.toLocaleDateString('zh-CN'); + + // 检查是否是保修到期字段 + if (field.fieldName === 'warrantyExpiry') { + const today = new Date(); + // 设置时间为同一天的00:00:00,确保只比较日期部分 + today.setHours(0, 0, 0, 0); + dateObj.setHours(0, 0, 0, 0); + + // 如果保修日期已过期,显示为红色 + if (dateObj < today) { + return {formattedDate}; + } + } + + return formattedDate; + }, }); } // 普通字段 @@ -552,6 +629,22 @@ function DeviceManagement() { + + }> @@ -681,7 +774,7 @@ function DeviceManagement() { control = ; break; case 'select': - // 特殊处理机柜选择 + // 特殊处理机柜选择(仍然使用单选) if (field.fieldName === 'rackId') { control = ( ); } else { + // 其他select类型字段改为使用复选框组支持多选 control = ( - + + + {field.options && field.options.map(option => ( + + {option.label} + + ))} + + ); } break; @@ -720,63 +816,7 @@ function DeviceManagement() { ); })} - {/* 自定义字段区域 */} - -
- {/* 添加自定义字段 */} - - setCustomFieldName(e.target.value)} - style={{ width: 150 }} - /> - setCustomFieldValue(e.target.value)} - style={{ width: 150 }} - /> - - - - {/* 显示自定义字段 */} -
- {Object.entries(form.getFieldValue('customFields') || {}).map(([key, value]) => ( -
- {key}: - {value} - -
- ))} -
-
-
+ @@ -847,7 +887,7 @@ function DeviceManagement() { width={600} destroyOnClose > - {!isImporting ? ( + {!isImporting && !importResult ? (

请上传CSV格式的设备数据文件

支持的编码格式:GBK

@@ -888,24 +928,50 @@ function DeviceManagement() {

正在导入数据...

- {importResult && ( + {importResult && importResult.statistics && (

导入完成:

-

总记录数:{importResult.statistics.total}

-

成功:{importResult.statistics.success}

-

失败:{importResult.statistics.failed}

- {importResult.statistics.errors.length > 0 && ( -
-

失败记录:

-
    - {importResult.statistics.errors.map((item, index) => ( -
  • - 行{item.row}:{item.error} -
  • - ))} -
-
- )} +

总记录数:{importResult.statistics.total || 0}

+

成功:{importResult.statistics.success || 0}

+

失败:{importResult.statistics.failed || 0}

+ {/* 显示导入失败记录详情表格 */} + {(() => { + // 检查是否有错误记录 + const errors = importResult.statistics?.errors; + const hasErrors = Array.isArray(errors) && errors.length > 0; + + // 即使没有errors字段,如果failed数量大于0,也应该提示用户 + const hasFailedRecords = importResult.statistics?.failed > 0; + + return (hasErrors || hasFailedRecords) && ( +
+

失败记录详情:

+ + {hasErrors ? ( + + + + + + + + + {errors.map((item, index) => ( + + + + + ))} + +
行号失败原因
{item.row || index + 1}{item.error || '未知错误'}
+ ) : ( +
+ 检测到 {importResult.statistics.failed} 条导入失败记录,但未提供详细错误信息 +
+ )} +
+ ); + })()}