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 } from '@ant-design/icons'; import axios from 'axios'; import dayjs from 'dayjs'; const { Option } = Select; const { RangePicker } = DatePicker; 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 [customFieldName, setCustomFieldName] = useState(''); const [customFieldValue, setCustomFieldValue] = useState(''); // 设备字段配置 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 [selectAll, setSelectAll] = useState(false); // 字段配置模态框 const [fieldConfigModalVisible, setFieldConfigModalVisible] = useState(false); // 获取所有设备(支持搜索、筛选和分页) 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'); setRacks(response.data); } 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); if (!deviceData.customFields) deviceData.customFields = {}; // 将customFields中的字段值合并到deviceData中,以便动态表单控件能正确显示 Object.entries(deviceData.customFields).forEach(([fieldName, value]) => { deviceData[fieldName] = value; }); form.setFieldsValue(deviceData); } 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 fixedFieldValues = {}; const dynamicFieldValues = {}; Object.entries(values).forEach(([key, value]) => { if (fixedFields.includes(key)) { fixedFieldValues[key] = value; } else if (key !== 'customFields') { dynamicFieldValues[key] = value; } }); // 合并原有的自定义字段和新的动态字段 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) { // 更新设备 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 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 handleDeviceSelect = (deviceId) => { setSelectedDevices(prev => { if (prev.includes(deviceId)) { return prev.filter(id => id !== deviceId); } else { return [...prev, deviceId]; } }); }; // 处理全选/取消全选 const handleSelectAll = (e) => { setSelectAll(e.target.checked); if (e.target.checked) { setSelectedDevices(devices.map(device => device.deviceId)); } else { setSelectedDevices([]); } }; // 导入设备数据 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); 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 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', }); // 添加机柜信息列 generatedColumns.push({ title: field.displayName, dataIndex: ['Rack', 'name'], key: field.fieldName, }); } // 特殊处理设备类型 else if (field.fieldName === 'type') { generatedColumns.push({ title: field.displayName, dataIndex: field.fieldName, key: field.fieldName, render: (type) => typeMap[type], }); } // 特殊处理状态字段 else if (field.fieldName === 'status') { generatedColumns.push({ title: field.displayName, dataIndex: field.fieldName, key: field.fieldName, render: (status) => ( {statusMap[status]?.text || status} ), }); } // 特殊处理日期字段 else if (field.fieldType === 'date') { generatedColumns.push({ title: field.displayName, dataIndex: field.fieldName, key: field.fieldName, render: (date) => date ? new Date(date).toLocaleDateString('zh-CN') : '', }); } // 普通字段 else { generatedColumns.push({ title: field.displayName, dataIndex: field.fieldName, key: field.fieldName, }); } }); // 添加操作列 generatedColumns.push({ title: '操作', key: 'action', render: (_, record) => ( ), }); return generatedColumns; }, [deviceFields]); // 保存字段配置 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)} />
0} onChange={handleSelectAll} indeterminate={selectedDevices.length > 0 && selectedDevices.length < devices.length} /> ), key: 'selection', width: 60, render: (_, record) => ( handleDeviceSelect(record.deviceId)} /> ) }, ...columns ]} dataSource={devices} rowKey="deviceId" loading={loading} pagination={pagination} onChange={handleTableChange} />
{/* 动态生成表单字段 */} {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 { control = ( ); } break; default: control = ; } return ( {control} ); })} {/* 自定义字段区域 */}
{/* 添加自定义字段 */} setCustomFieldName(e.target.value)} style={{ width: 150 }} /> setCustomFieldValue(e.target.value)} style={{ width: 150 }} /> {/* 显示自定义字段 */}
{Object.entries(form.getFieldValue('customFields') || {}).map(([key, value]) => (
{key}: {value}
))}
{/* 导入设备模态框 */} {/* 字段配置模态框 */} 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} destroyOnClose > {!isImporting ? (

请上传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.total}

成功:{importResult.statistics.success}

失败:{importResult.statistics.failed}

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

失败记录:

    {importResult.statistics.errors.map((item, index) => (
  • 行{item.row}:{item.error}
  • ))}
)}
)}
)}
); } export default DeviceManagement;