import React, { useState, useEffect } from 'react'; import { Table, Button, Modal, Form, Input, Select, DatePicker, message, Card, Space, InputNumber, Switch, Upload, Progress, Checkbox } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, UploadOutlined, DownloadOutlined, SettingOutlined, UndoOutlined, CloudServerOutlined, SwapOutlined, SafetyOutlined, DatabaseOutlined, AppstoreOutlined } from '@ant-design/icons'; import axios from 'axios'; import dayjs from 'dayjs'; const { Option } = Select; const { RangePicker } = DatePicker; // 可调整列宽的表头组件 const ResizeableTitle = (props) => { const { onResize, width, ...restProps } = props; if (!width) { return ; } const handleMouseDown = (e) => { if (!onResize) return; const startX = e.pageX; const startWidth = width; const handleMouseMove = (moveEvent) => { const diff = moveEvent.pageX - startX; const newWidth = Math.max(50, startWidth + diff); // 最小宽度50px onResize(newWidth); }; const handleMouseUp = () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }; return ( {restProps.children}
); }; function DeviceManagement() { const [devices, setDevices] = useState([]); const [racks, setRacks] = useState([]); const [loading, setLoading] = useState(true); const [modalVisible, setModalVisible] = useState(false); const [editingDevice, setEditingDevice] = useState(null); const [form] = Form.useForm(); // 搜索和筛选状态 const [keyword, setKeyword] = useState(''); const [status, setStatus] = useState('all'); const [type, setType] = useState('all'); const [searchForm] = Form.useForm(); // 分页状态 const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, pageSizeOptions: ['10', '20', '30', '50', '100'], showSizeChanger: true, showTotal: (total) => `共 ${total} 条记录` }); // 设备字段配置 const [deviceFields, setDeviceFields] = useState([]); const [loadingFields, setLoadingFields] = useState(true); // 导入导出状态 const [importModalVisible, setImportModalVisible] = useState(false); const [importProgress, setImportProgress] = useState(0); const [isImporting, setIsImporting] = useState(false); const [importResult, setImportResult] = useState(null); const [selectedDevices, setSelectedDevices] = useState([]); const [detailModalVisible, setDetailModalVisible] = useState(false); const [selectedDevice, setSelectedDevice] = useState(null); // 自定义字段状态 const [customFieldName, setCustomFieldName] = useState(''); const [customFieldValue, setCustomFieldValue] = useState(''); // 字段配置模态框 const [fieldConfigModalVisible, setFieldConfigModalVisible] = useState(false); // 列宽状态 const [columnWidths, setColumnWidths] = useState({}); // 获取所有设备(支持搜索、筛选和分页) const fetchDevices = async (page = 1, pageSize = 10, searchParams = {}) => { try { setLoading(true); // 构建查询参数 const params = { page, pageSize, keyword: searchParams.keyword || keyword, status: searchParams.status || status, type: searchParams.type || type }; const response = await axios.get('/api/devices', { params }); const { devices, total } = response.data; // 将customFields中的字段值映射为设备对象的直接属性 const processedDevices = devices.map(device => { const deviceWithFields = { ...device }; // 如果有自定义字段,将其展开为设备对象的直接属性 if (device.customFields && typeof device.customFields === 'object') { Object.entries(device.customFields).forEach(([fieldName, value]) => { deviceWithFields[fieldName] = value; }); } return deviceWithFields; }); setDevices(processedDevices); setPagination(prev => ({ ...prev, current: page, pageSize, total })); } catch (error) { message.error('获取设备列表失败'); console.error('获取设备列表失败:', error); } finally { setLoading(false); } }; // 获取设备字段配置 const fetchDeviceFields = async () => { try { setLoadingFields(true); const response = await axios.get('/api/deviceFields'); // 按顺序排序字段 const sortedFields = response.data.sort((a, b) => a.order - b.order); setDeviceFields(sortedFields); } catch (error) { message.error('获取字段配置失败'); console.error('获取字段配置失败:', error); // 如果获取失败,使用默认字段配置 setDeviceFields(defaultDeviceFields); } finally { setLoadingFields(false); } }; // 默认设备字段配置 const defaultDeviceFields = [ { fieldName: 'deviceId', displayName: '设备ID', fieldType: 'string', required: true, order: 1, visible: true }, { fieldName: 'name', displayName: '设备名称', fieldType: 'string', required: true, order: 2, visible: true }, { fieldName: 'type', displayName: '设备类型', fieldType: 'select', required: true, order: 3, visible: true, options: [{ value: 'server', label: '服务器' }, { value: 'switch', label: '交换机' }, { value: 'router', label: '路由器' }, { value: 'storage', label: '存储设备' }, { value: 'other', label: '其他设备' }] }, { fieldName: 'model', displayName: '型号', fieldType: 'string', required: true, order: 4, visible: true }, { fieldName: 'serialNumber', displayName: '序列号', fieldType: 'string', required: true, order: 5, visible: true }, { fieldName: 'rackId', displayName: '所在机柜', fieldType: 'select', required: true, order: 6, visible: true }, { fieldName: 'position', displayName: '位置(U)', fieldType: 'number', required: true, order: 7, visible: true }, { fieldName: 'height', displayName: '高度(U)', fieldType: 'number', required: true, order: 8, visible: true }, { fieldName: 'powerConsumption', displayName: '功率(W)', fieldType: 'number', required: true, order: 9, visible: true }, { fieldName: 'status', displayName: '状态', fieldType: 'select', required: true, order: 10, visible: true, options: [{ value: 'running', label: '运行中' }, { value: 'maintenance', label: '维护中' }, { value: 'offline', label: '离线' }, { value: 'fault', label: '故障' }] }, { fieldName: 'purchaseDate', displayName: '购买日期', fieldType: 'date', required: true, order: 11, visible: true }, { fieldName: 'warrantyExpiry', displayName: '保修到期', fieldType: 'date', required: true, order: 12, visible: true }, { fieldName: 'ipAddress', displayName: 'IP地址', fieldType: 'string', required: false, order: 13, visible: true }, { fieldName: 'description', displayName: '描述', fieldType: 'textarea', required: false, order: 14, visible: true } ]; // 获取所有机柜 const fetchRacks = async () => { try { const response = await axios.get('/api/racks'); // 现在API返回的格式是 { racks: [], total: number } setRacks(response.data.racks || []); } catch (error) { message.error('获取机柜列表失败'); console.error('获取机柜列表失败:', error); } }; useEffect(() => { fetchDevices(); fetchRacks(); fetchDeviceFields(); }, []); // 打开模态框 const showModal = (device = null) => { setEditingDevice(device); if (device) { // 转换日期字段为dayjs格式 const deviceData = { ...device }; if (deviceData.purchaseDate) deviceData.purchaseDate = dayjs(deviceData.purchaseDate); if (deviceData.warrantyExpiry) deviceData.warrantyExpiry = dayjs(deviceData.warrantyExpiry); // 定义设备模型的固定字段 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]; } }); // 将自定义字段直接添加到表单数据中(不在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(); } setModalVisible(true); }; // 关闭模态框 const handleCancel = () => { setModalVisible(false); setEditingDevice(null); }; // 提交表单 const handleSubmit = async (values) => { try { // 定义设备模型的固定字段 const fixedFields = [ 'deviceId', 'name', 'type', 'model', 'serialNumber', 'rackId', 'position', 'height', 'powerConsumption', 'status', 'purchaseDate', 'warrantyExpiry', 'ipAddress', 'description' ]; // 构建最终的设备数据,包含固定字段和自定义字段 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.keys(deviceData).forEach(key => { if (!fixedFields.includes(key) && key !== 'customFields') { // 将非固定字段移动到customFields对象中 deviceData.customFields[key] = deviceData[key]; delete deviceData[key]; } }); if (editingDevice) { // 更新设备 await axios.put(`/api/devices/${editingDevice.deviceId}`, deviceData); message.success('设备更新成功'); } else { // 创建设备 await axios.post('/api/devices', deviceData); message.success('设备创建成功'); } setModalVisible(false); fetchDevices(); setEditingDevice(null); } catch (error) { message.error(editingDevice ? '设备更新失败' : '设备创建失败'); console.error(editingDevice ? '设备更新失败:' : '设备创建失败:', error); } }; // 搜索处理函数 const handleSearch = (values) => { fetchDevices(1, 10, values); }; // 重置筛选条件 const handleReset = () => { setKeyword(''); setStatus('all'); setType('all'); searchForm.resetFields(); fetchDevices(1, pagination.pageSize, { keyword: '', status: 'all', type: 'all' }); }; // 表格分页变化处理 const handleTableChange = (pagination) => { setPagination(pagination); fetchDevices(pagination.current, pagination.pageSize); }; // 批量下线设备 const handleBatchOffline = async () => { Modal.confirm({ title: '批量下线确认', content: `确定要将选中的 ${selectedDevices.length} 个设备下线吗?`, okText: '确认下线', okType: 'primary', cancelText: '取消', onOk: async () => { try { const response = await axios.put('/api/devices/batch-offline', { deviceIds: selectedDevices }); message.success(response.data.message || '批量下线成功'); setSelectedDevices([]); setSelectAll(false); fetchDevices(); } catch (error) { message.error('批量下线失败'); console.error('批量下线设备失败:', error); } } }); }; // 批量删除设备 const handleBatchDelete = async () => { Modal.confirm({ title: '批量删除确认', content: `确定要删除选中的 ${selectedDevices.length} 个设备吗?此操作不可恢复!`, okText: '确认删除', okType: 'danger', cancelText: '取消', onOk: async () => { try { const response = await axios.delete('/api/devices/batch-delete', { data: { deviceIds: selectedDevices } }); message.success(response.data.message || '批量删除成功'); setSelectedDevices([]); setSelectAll(false); fetchDevices(); } catch (error) { message.error('批量删除失败'); console.error('批量删除设备失败:', error); } } }); }; // 删除设备 const handleDelete = async (deviceId) => { Modal.confirm({ title: '确认删除', content: '确定要删除这个设备吗?', okText: '删除', okType: 'danger', cancelText: '取消', onOk: async () => { try { await axios.delete(`/api/devices/${deviceId}`); message.success('设备删除成功'); fetchDevices(); } catch (error) { message.error('设备删除失败'); console.error('设备删除失败:', error); } } }); }; // 显示设备详情 const handleShowDetail = (device) => { setSelectedDevice(device); setDetailModalVisible(true); }; // 导出设备数据 const handleExport = async () => { try { if (selectedDevices.length === 0) { message.warning('请先选择要导出的设备'); return; } const params = new URLSearchParams(); selectedDevices.forEach(id => params.append('deviceIds', id)); const response = await axios.get(`/api/devices/export?${params.toString()}`, { responseType: 'blob' }); const blob = new Blob([response.data], { type: 'text/csv; charset=gbk' }); const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `devices_export_${new Date().toISOString().split('T')[0]}.csv`; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); message.success('导出成功'); } catch (error) { message.error('导出失败'); console.error('导出设备失败:', error); } }; // 导入设备数据 const handleImport = async (file) => { try { setIsImporting(true); setImportProgress(0); setImportResult(null); const formData = new FormData(); formData.append('csvFile', file); // 模拟进度 const progressInterval = setInterval(() => { setImportProgress(prev => { if (prev >= 90) { clearInterval(progressInterval); return prev; } return prev + 10; }); }, 500); const response = await axios.post('/api/devices/import', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); clearInterval(progressInterval); setImportProgress(100); setImportResult(response.data); setIsImporting(false); // 根据导入结果显示不同的消息 const { success, failed } = response.data.statistics; if (failed > 0) { message.warning(`导入完成,但有 ${failed} 条记录导入失败`); } else { message.success('所有记录导入成功'); } // 重新加载设备列表 setTimeout(() => { fetchDevices(); }, 1000); } catch (error) { setIsImporting(false); message.error('导入失败'); console.error('导入设备失败:', error); } // 阻止默认上传行为 return false; }; // 状态标签映射 const statusMap = { running: { text: '运行中', color: 'green' }, maintenance: { text: '维护中', color: 'orange' }, offline: { text: '离线', color: 'gray' }, fault: { text: '故障', color: 'red' } }; // 设备类型映射 const typeMap = { server: '服务器', switch: '交换机', router: '路由器', storage: '存储设备', other: '其他设备' }; // 获取设备类型图标 const getDeviceTypeIcon = (type) => { const iconMap = { server: , switch: , router: , storage: , other: }; return iconMap[type] || ; }; // 处理列宽变化 const handleColumnResize = (key, width) => { setColumnWidths(prev => ({ ...prev, [key]: width })); }; // 重置列宽到默认值 const resetColumnWidths = () => { setColumnWidths({}); message.success('列宽已重置为默认值'); }; // 处理表头单元格拖拽 const handleHeaderCellResize = (key) => (column) => ({ width: column.width, onResize: (width) => handleColumnResize(key, width), }); // 动态生成表格列配置 const columns = React.useMemo(() => { const generatedColumns = []; // 根据字段配置动态生成列,只显示visible为true的字段 deviceFields.forEach(field => { // 只处理可见字段 if (!field.visible) return; // 特殊处理机柜字段 if (field.fieldName === 'rackId') { // 添加机房信息列 generatedColumns.push({ title: '所在机房', dataIndex: ['Rack', 'Room', 'name'], key: 'roomName', width: columnWidths.roomName || 120, onHeaderCell: handleHeaderCellResize('roomName'), }); // 添加机柜信息列 generatedColumns.push({ title: field.displayName, dataIndex: ['Rack', 'name'], key: field.fieldName, width: columnWidths[field.fieldName] || 120, onHeaderCell: handleHeaderCellResize(field.fieldName), }); } // 特殊处理设备类型 else if (field.fieldName === 'type') { generatedColumns.push({ title: field.displayName, dataIndex: field.fieldName, key: field.fieldName, width: columnWidths[field.fieldName] || 100, onHeaderCell: handleHeaderCellResize(field.fieldName), render: (type) => { return ( {getDeviceTypeIcon(type)} {typeMap[type]} ); }, }); } // 特殊处理状态字段 else if (field.fieldName === 'status') { generatedColumns.push({ title: field.displayName, dataIndex: field.fieldName, key: field.fieldName, width: columnWidths[field.fieldName] || 100, onHeaderCell: handleHeaderCellResize(field.fieldName), render: (status) => { if (Array.isArray(status)) { return ( {status.map(s => ( {statusMap[s]?.text || s} ))} ); } return ( {statusMap[status]?.text || status} ); }, }); } // 特殊处理日期字段 else if (field.fieldType === 'date') { generatedColumns.push({ title: field.displayName, dataIndex: field.fieldName, key: field.fieldName, width: columnWidths[field.fieldName] || 120, onHeaderCell: handleHeaderCellResize(field.fieldName), 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; }, }); } // 普通字段 else { // 为不同类型的字段设置不同的默认宽度 let defaultWidth = 120; if (field.fieldName === 'deviceId' || field.fieldName === 'name') { defaultWidth = 150; } else if (field.fieldName === 'description') { defaultWidth = 200; } else if (field.fieldType === 'number') { defaultWidth = 80; } else if (field.fieldType === 'textarea') { defaultWidth = 180; } // 为设备名称和ID列添加点击查看详情功能 const columnConfig = { title: field.displayName, dataIndex: field.fieldName, key: field.fieldName, width: columnWidths[field.fieldName] || defaultWidth, onHeaderCell: handleHeaderCellResize(field.fieldName), }; // 设备名称和ID列添加点击效果 if (field.fieldName === 'deviceId' || field.fieldName === 'name') { columnConfig.render = (value, record) => ( handleShowDetail(record)} style={{ color: '#1890ff', textDecoration: 'none', cursor: 'pointer' }} onMouseEnter={(e) => e.target.style.textDecoration = 'underline'} onMouseLeave={(e) => e.target.style.textDecoration = 'none'} > {value || '-'} ); } generatedColumns.push(columnConfig); } }); // 添加操作列 generatedColumns.push({ title: '操作', key: 'action', width: columnWidths.action || 120, onHeaderCell: handleHeaderCellResize('action'), render: (_, record) => ( ), }); return generatedColumns; }, [deviceFields, columnWidths]); // 保存字段配置 const handleSaveFieldConfig = async (values) => { try { // 更新设备字段配置的可见性 const updatedFields = deviceFields.map(field => ({ ...field, visible: values[field.fieldName] })); // 保存到后端 await axios.post('/api/deviceFields/config', updatedFields); // 更新本地状态 setDeviceFields(updatedFields); message.success('字段配置保存成功'); setFieldConfigModalVisible(false); } catch (error) { message.error('字段配置保存失败'); console.error('保存字段配置失败:', error); } }; // 重置字段配置为默认值 const handleResetFieldConfig = () => { const defaultFields = defaultDeviceFields; setDeviceFields(defaultFields); message.success('字段配置已重置为默认值'); }; return (
}> {/* 搜索和筛选区域 */}
} style={{ width: 300 }} value={keyword} onChange={(e) => setKeyword(e.target.value)} />
{/* 动态生成表单字段 */} {deviceFields.map(field => { // 根据字段类型生成表单控件 let control = null; switch (field.fieldType) { case 'text': case 'string': control = ; break; case 'number': control = ; break; case 'boolean': control = ; break; case 'date': control = ; break; case 'textarea': control = ; break; case 'select': // 特殊处理机柜选择(仍然使用单选) if (field.fieldName === 'rackId') { control = ( ); } else { // 设备类型等使用单选select control = ( ); } break; default: control = ; } return ( {control} ); })}
{/* 导入设备模态框 */} {/* 字段配置模态框 */} setFieldConfigModalVisible(false)} footer={null} width={600} >
({ ...acc, [field.fieldName]: field.visible }), {})} >
{/* 将字段分为多列显示 */}
{deviceFields.map(field => ( {field.displayName} ))}
{ setImportModalVisible(false); setImportProgress(0); setImportResult(null); setIsImporting(false); }} footer={null} width={600} destroyOnHidden > {!isImporting && !importResult ? (

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

支持的编码格式:GBK

CSV文件格式要求:

  • 必填字段:设备ID、设备名称、设备类型、型号、序列号、所在机柜ID、位置(U)、高度(U)、功率(W)、状态、购买日期、保修到期
  • 可选字段:IP地址、描述
  • 设备类型:server(服务器)、switch(交换机)、router(路由器)、storage(存储设备)
  • 状态值:running(运行中)、maintenance(维护中)、offline(离线)、fault(故障)
  • 日期格式:YYYY-MM-DD (例如:2023-01-01)
包含示例数据的CSV模板文件
) : (

正在导入数据...

{importResult && importResult.statistics && (

导入完成:

总记录数:{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} 条导入失败记录,但未提供详细错误信息
)}
); })()}
)} )} {/* 设备详情模态框 */} { setDetailModalVisible(false); setSelectedDevice(null); }} footer={[ , ]} width={800} destroyOnHidden > {selectedDevice && (
{/* 基本信息区域 */}
{selectedDevice.deviceId || '-'}
{selectedDevice.name || '-'}
{selectedDevice.type ? ( {getDeviceTypeIcon(selectedDevice.type)} {typeMap[selectedDevice.type] || selectedDevice.type} ) : '-'}
{selectedDevice.model || '-'}
{selectedDevice.serialNumber || '-'}
{selectedDevice.ipAddress || '-'}
{selectedDevice.Rack?.Room?.name || '-'}
{selectedDevice.Rack?.name || '-'}
{selectedDevice.position || '-'}
{selectedDevice.height || '-'}
{selectedDevice.power || '-'}
{selectedDevice.status ? statusMap[selectedDevice.status]?.text || selectedDevice.status : '-'}
{selectedDevice.purchaseDate ? new Date(selectedDevice.purchaseDate).toLocaleDateString('zh-CN') : '-'}
{selectedDevice.warrantyExpiry ? new Date(selectedDevice.warrantyExpiry).toLocaleDateString('zh-CN') : '-'}
{selectedDevice.description && (
{selectedDevice.description}
)}
)}
); } export default DeviceManagement;