更新了设备管理的功能,一键删除,修复了编辑字段的bug,修复了导入设备的功能等

This commit is contained in:
zhang1106
2025-12-19 13:21:39 +08:00
parent 6f53a50fa7
commit e363553e90
2 changed files with 265 additions and 160 deletions
+79 -40
View File
@@ -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 = {
+186 -120
View File
@@ -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 (
<Space>
{type.map(t => (
<span key={t}>{typeMap[t]}</span>
))}
</Space>
);
}
return typeMap[type];
},
});
}
// 特殊处理状态字段
@@ -470,11 +514,24 @@ function DeviceManagement() {
title: field.displayName,
dataIndex: field.fieldName,
key: field.fieldName,
render: (status) => (
<span style={{ color: statusMap[status]?.color || 'black' }}>
{statusMap[status]?.text || status}
</span>
),
render: (status) => {
if (Array.isArray(status)) {
return (
<Space>
{status.map(s => (
<span key={s} style={{ color: statusMap[s]?.color || 'black' }}>
{statusMap[s]?.text || s}
</span>
))}
</Space>
);
}
return (
<span style={{ color: statusMap[status]?.color || 'black' }}>
{statusMap[status]?.text || status}
</span>
);
},
});
}
// 特殊处理日期字段
@@ -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 <span style={{ color: '#d93025', fontWeight: 'bold' }}>{formattedDate}</span>;
}
}
return formattedDate;
},
});
}
// 普通字段
@@ -552,6 +629,22 @@ function DeviceManagement() {
<Button icon={<DownloadOutlined />} onClick={handleExport}>导出设备</Button>
<Button icon={<UploadOutlined />} onClick={() => setImportModalVisible(true)}>导入设备</Button>
<Button icon={<SettingOutlined />} onClick={() => setFieldConfigModalVisible(true)}>字段配置</Button>
<Button
type="primary"
danger={false}
disabled={selectedDevices.length === 0}
onClick={handleBatchOffline}
>
一键下线 ({selectedDevices.length})
</Button>
<Button
type="primary"
danger
disabled={selectedDevices.length === 0}
onClick={handleBatchDelete}
>
一键删除 ({selectedDevices.length})
</Button>
</Space>
}>
@@ -681,7 +774,7 @@ function DeviceManagement() {
control = <Input.TextArea placeholder={`请输入${field.displayName}`} rows={3} />;
break;
case 'select':
// 特殊处理机柜选择
// 特殊处理机柜选择(仍然使用单选)
if (field.fieldName === 'rackId') {
control = (
<Select placeholder={`请选择${field.displayName}`}>
@@ -693,14 +786,17 @@ function DeviceManagement() {
</Select>
);
} else {
// 其他select类型字段改为使用复选框组支持多选
control = (
<Select placeholder={`请选择${field.displayName}`}>
{field.options && field.options.map(option => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
))}
</Select>
<Checkbox.Group placeholder={`请选择${field.displayName}`}>
<Space direction="vertical">
{field.options && field.options.map(option => (
<Checkbox key={option.value} value={option.value}>
{option.label}
</Checkbox>
))}
</Space>
</Checkbox.Group>
);
}
break;
@@ -720,63 +816,7 @@ function DeviceManagement() {
);
})}
{/* 自定义字段区域 */}
<Form.Item label="自定义字段">
<div>
{/* 添加自定义字段 */}
<Space style={{ marginBottom: 16 }}>
<Input
placeholder="字段名称"
value={customFieldName}
onChange={(e) => setCustomFieldName(e.target.value)}
style={{ width: 150 }}
/>
<Input
placeholder="字段值"
value={customFieldValue}
onChange={(e) => setCustomFieldValue(e.target.value)}
style={{ width: 150 }}
/>
<Button
type="primary"
onClick={() => {
if (customFieldName && customFieldValue) {
const currentCustomFields = form.getFieldValue('customFields') || {};
form.setFieldValue('customFields', {
...currentCustomFields,
[customFieldName]: customFieldValue
});
setCustomFieldName('');
setCustomFieldValue('');
}
}}
>
添加字段
</Button>
</Space>
{/* 显示自定义字段 */}
<div>
{Object.entries(form.getFieldValue('customFields') || {}).map(([key, value]) => (
<div key={key} style={{ marginBottom: 8, display: 'flex', alignItems: 'center' }}>
<span style={{ marginRight: 8, fontWeight: 'bold' }}>{key}:</span>
<span style={{ marginRight: 16 }}>{value}</span>
<Button
danger
size="small"
onClick={() => {
const currentCustomFields = { ...form.getFieldValue('customFields') };
delete currentCustomFields[key];
form.setFieldValue('customFields', currentCustomFields);
}}
>
删除
</Button>
</div>
))}
</div>
</div>
</Form.Item>
<Form.Item style={{ textAlign: 'right' }}>
<Space>
@@ -847,7 +887,7 @@ function DeviceManagement() {
width={600}
destroyOnClose
>
{!isImporting ? (
{!isImporting && !importResult ? (
<div>
<p>请上传CSV格式的设备数据文件</p>
<p style={{ color: '#999', fontSize: '12px', marginBottom: 20 }}>支持的编码格式GBK</p>
@@ -888,24 +928,50 @@ function DeviceManagement() {
<div>
<p>正在导入数据...</p>
<Progress percent={importProgress} status="active" style={{ marginBottom: 20 }} />
{importResult && (
{importResult && importResult.statistics && (
<div>
<p style={{ marginBottom: 10 }}>导入完成</p>
<p>总记录数{importResult.statistics.total}</p>
<p>成功{importResult.statistics.success}</p>
<p>失败{importResult.statistics.failed}</p>
{importResult.statistics.errors.length > 0 && (
<div style={{ marginTop: 20, maxHeight: 300, overflowY: 'auto' }}>
<h4>失败记录</h4>
<ul>
{importResult.statistics.errors.map((item, index) => (
<li key={index} style={{ color: 'red', fontSize: '12px', marginBottom: 5 }}>
{item.row}{item.error}
</li>
))}
</ul>
</div>
)}
<p>总记录数{importResult.statistics.total || 0}</p>
<p>成功{importResult.statistics.success || 0}</p>
<p>失败{importResult.statistics.failed || 0}</p>
{/* 显示导入失败记录详情表格 */}
{(() => {
// 检查是否有错误记录
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) && (
<div style={{ marginTop: 20, maxHeight: 400, overflowY: 'auto', border: '1px solid #ffcccc', borderRadius: '4px', padding: '12px', backgroundColor: '#fff7f7' }}>
<h4 style={{ color: '#d93025', marginBottom: '12px' }}>失败记录详情</h4>
{hasErrors ? (
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '13px' }}>
<thead>
<tr style={{ backgroundColor: '#ffeeee' }}>
<th style={{ border: '1px solid #ffcccc', padding: '8px', textAlign: 'left', width: '80px' }}>行号</th>
<th style={{ border: '1px solid #ffcccc', padding: '8px', textAlign: 'left' }}>失败原因</th>
</tr>
</thead>
<tbody>
{errors.map((item, index) => (
<tr key={index} style={{ borderBottom: '1px solid #ffcccc' }}>
<td style={{ border: '1px solid #ffcccc', padding: '8px', fontWeight: 'bold' }}>{item.row || index + 1}</td>
<td style={{ border: '1px solid #ffcccc', padding: '8px', color: '#d93025' }}>{item.error || '未知错误'}</td>
</tr>
))}
</tbody>
</table>
) : (
<div style={{ color: '#d93025', padding: '20px', textAlign: 'center' }}>
检测到 {importResult.statistics.failed} 条导入失败记录但未提供详细错误信息
</div>
)}
</div>
);
})()}
<Button type="primary" onClick={() => {
setImportModalVisible(false);
setImportProgress(0);