import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { Table, Button, Modal, Form, Input, Select, DatePicker, message, Card, Space, InputNumber, Switch, Upload, Progress, Checkbox, Spin, Dropdown, Tooltip } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, UploadOutlined, DownloadOutlined, SettingOutlined, UndoOutlined, CloudServerOutlined, SwapOutlined, SafetyOutlined, DatabaseOutlined, AppstoreOutlined, MoreOutlined, ReloadOutlined, ExportOutlined, DragOutlined, FileExcelOutlined } from '@ant-design/icons'; import axios from 'axios'; import dayjs from 'dayjs'; const { Option } = Select; const { RangePicker } = DatePicker; // 防抖 Hook function useDebounce(value, delay) { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay); return () => { clearTimeout(handler); }; }, [value, delay]); return debouncedValue; } // 工具函数提取到组件外部,避免每次渲染重复创建 const getStatusConfig = (status) => { const statusMap = { running: { text: '运行中', color: 'green' }, maintenance: { text: '维护中', color: 'orange' }, offline: { text: '离线', color: 'gray' }, fault: { text: '故障', color: 'red' } }; return statusMap[status] || { text: status, color: 'black' }; }; const getTypeLabel = (type) => { const typeMap = { server: '服务器', switch: '交换机', router: '路由器', storage: '存储设备', other: '其他设备' }; return typeMap[type] || type; }; const getDeviceTypeIcon = (type) => { const iconMap = { server: , switch: , router: , storage: , other: }; return iconMap[type] || ; }; // 格式化日期 const formatDate = (date, fieldName) => { if (!date) return ''; const dateObj = new Date(date); const formattedDate = dateObj.toLocaleDateString('zh-CN'); if (fieldName === 'warrantyExpiry') { const today = new Date(); today.setHours(0, 0, 0, 0); dateObj.setHours(0, 0, 0, 0); if (dateObj < today) { return {formattedDate}; } } return formattedDate; }; // 默认设备字段配置 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 ResizableTitle = (props) => { const { children, onResize, width, ...restProps } = props; const handleMouseDown = (e) => { if (!onResize) return; e.preventDefault(); e.stopPropagation(); const th = e.currentTarget.closest('th'); if (!th) return; const startWidth = th.offsetWidth; const startX = e.clientX; const handleMouseMove = (moveEvent) => { const diff = moveEvent.clientX - startX; const newWidth = Math.max(50, startWidth + diff); onResize(newWidth); }; const handleMouseUp = () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }; return (
{children} {onResize && (
)}
); }; function DeviceManagement() { const [devices, setDevices] = useState([]); const [allDevices, setAllDevices] = useState([]); const [racks, setRacks] = useState([]); const [loading, setLoading] = useState(true); const [searching, setSearching] = useState(false); 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 [importPhase, setImportPhase] = useState(''); 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 [batchStatusModalVisible, setBatchStatusModalVisible] = useState(false); const [batchStatusLoading, setBatchStatusLoading] = useState(false); const [batchStatusForm] = Form.useForm(); // 批量移动模态框 const [batchMoveModalVisible, setBatchMoveModalVisible] = useState(false); const [batchMoveLoading, setBatchMoveLoading] = useState(false); const [batchMoveForm] = Form.useForm(); // 导出选项模态框 const [exportModalVisible, setExportModalVisible] = useState(false); const [exportFormat, setExportFormat] = useState('csv'); const [exportScope, setExportScope] = useState('selected'); const [exportFields, setExportFields] = useState([]); const [exportLoading, setExportLoading] = useState(false); const [currentPageDevices, setCurrentPageDevices] = useState([]); // 全选状态 const [selectAll, setSelectAll] = useState(false); // 列宽状态 const [columnWidths, setColumnWidths] = useState({}); // 缓存用于搜索的数据(避免重复处理) const devicesCacheRef = useRef({ timestamp: 0, data: null, TTL: 5 * 60 * 1000 // 缓存5分钟 }); // 防抖搜索关键词 const debouncedKeyword = useDebounce(keyword, 300); // 预计算所有设备的搜索索引(提升搜索性能) const searchIndexRef = useRef(new Map()); // 构建设备搜索索引 const buildSearchIndex = useCallback((devices) => { const index = new Map(); devices.forEach((device, idx) => { const searchableValues = []; // 收集所有基本类型字段值 Object.entries(device).forEach(([key, value]) => { if (value === null || value === undefined) return; if (typeof value === 'object') { // 收集嵌套对象值 if (device.Rack?.name) searchableValues.push(String(device.Rack.name).toLowerCase()); if (device.Rack?.Room?.name) searchableValues.push(String(device.Rack.Room.name).toLowerCase()); // 收集自定义字段值 if (device.customFields && typeof device.customFields === 'object') { Object.values(device.customFields).forEach(cfValue => { if (cfValue !== null && cfValue !== undefined && typeof cfValue !== 'object') { searchableValues.push(String(cfValue).toLowerCase()); } }); } } else { searchableValues.push(String(value).toLowerCase()); } }); index.set(idx, searchableValues); }); return index; }, []); // 优化的全字段搜索函数 const searchDevices = useCallback((devices, keyword) => { if (!keyword || !keyword.trim()) { return devices; } const searchTerm = keyword.toLowerCase().trim(); return devices.filter((device, idx) => { // 使用预计算的搜索索引 let searchableValues = searchIndexRef.current.get(idx); if (!searchableValues) { // 如果没有预计算索引,当场计算并缓存 searchableValues = []; Object.entries(device).forEach(([key, value]) => { if (value === null || value === undefined) return; if (typeof value === 'object') { if (device.Rack?.name) searchableValues.push(String(device.Rack.name).toLowerCase()); if (device.Rack?.Room?.name) searchableValues.push(String(device.Rack.Room.name).toLowerCase()); if (device.customFields && typeof device.customFields === 'object') { Object.values(device.customFields).forEach(cfValue => { if (cfValue !== null && cfValue !== undefined && typeof cfValue !== 'object') { searchableValues.push(String(cfValue).toLowerCase()); } }); } } else { searchableValues.push(String(value).toLowerCase()); } }); searchIndexRef.current.set(idx, searchableValues); } return searchableValues.some(value => value.includes(searchTerm)); }); }, []); // 获取所有设备数据(不分页,用于本地搜索)- 使用缓存 const fetchAllDevices = useCallback(async (forceRefresh = false) => { const now = Date.now(); const cache = devicesCacheRef.current; // 检查缓存是否有效 if (!forceRefresh && cache.data && (now - cache.timestamp) < cache.TTL) { return cache.data; } try { const response = await axios.get('/api/devices', { params: { page: 1, pageSize: 99999 } }); const { devices } = 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; }); // 更新缓存 cache.data = processedDevices; cache.timestamp = now; // 预计算搜索索引 searchIndexRef.current = buildSearchIndex(processedDevices); return processedDevices; } catch (error) { console.error('获取所有设备数据失败:', error); return cache.data || []; } }, [buildSearchIndex]); // 使用 useMemo 缓存筛选后的设备数据 const filteredDevicesMemo = useMemo(() => { if (!allDevices.length) return []; let result = allDevices; // 状态筛选 if (status && status !== 'all') { result = result.filter(device => device.status === status); } // 类型筛选 if (type && type !== 'all') { result = result.filter(device => device.type === type); } // 关键词搜索(使用防抖后的关键词) if (debouncedKeyword && debouncedKeyword.trim()) { result = searchDevices(result, debouncedKeyword); } return result; }, [allDevices, status, type, debouncedKeyword, searchDevices]); // 获取所有设备(支持搜索、筛选和分页)- 使用缓存和useCallback const fetchDevices = useCallback(async (page = 1, pageSize = 10, forceRefresh = false) => { try { setLoading(true); // 先获取所有设备数据(使用缓存,批量删除后强制刷新) const allData = await fetchAllDevices(forceRefresh); setAllDevices(allData); // 更新分页信息(筛选后的数据会通过useMemo自动更新) setPagination(prev => ({ ...prev, current: page, pageSize, total: filteredDevicesMemo.length })); } catch (error) { message.error('获取设备列表失败'); console.error('获取设备列表失败:', error); } finally { setLoading(false); } }, [fetchAllDevices, filteredDevicesMemo.length]); // 获取设备字段配置 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(); }, []); // 同步当前页设备数据 useEffect(() => { if (filteredDevicesMemo.length > 0) { const start = (pagination.current - 1) * pagination.pageSize; const end = start + pagination.pageSize; const currentPageData = filteredDevicesMemo.slice(start, end); setCurrentPageDevices(currentPageData); } else { setCurrentPageDevices([]); } }, [filteredDevicesMemo, pagination.current, pagination.pageSize]); // 打开模态框 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) => { setSearching(true); setKeyword(values.keyword || ''); setStatus(values.status || 'all'); setType(values.type || 'all'); // 筛选后的数据会通过useMemo自动更新,这里只需更新分页 setPagination(prev => ({ ...prev, current: 1 })); // 短暂延迟后移除搜索状态,提供视觉反馈 setTimeout(() => setSearching(false), 300); }; // 重置筛选条件 const handleReset = () => { setSearching(true); setKeyword(''); setStatus('all'); setType('all'); searchForm.resetFields(); // 短暂延迟后移除搜索状态 setTimeout(() => setSearching(false), 300); }; // 表格分页变化处理 const handleTableChange = (newPagination) => { setPagination(newPagination); const start = (newPagination.current - 1) * newPagination.pageSize; const end = start + newPagination.pageSize; const currentPageData = filteredDevicesMemo.slice(start, end); setCurrentPageDevices(currentPageData); fetchDevices(newPagination.current, newPagination.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(1, 10, true); } 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 showBatchStatusModal = () => { if (selectedDevices.length === 0) { message.warning('请先选择要操作的设备'); return; } batchStatusForm.resetFields(); setBatchStatusModalVisible(true); }; // 执行批量状态变更 const handleBatchStatusChange = async () => { try { const values = await batchStatusForm.validateFields(); setBatchStatusLoading(true); const response = await axios.put('/api/devices/batch-status', { deviceIds: selectedDevices, status: values.status }); message.success(response.data.message || '批量状态变更成功'); setBatchStatusModalVisible(false); setSelectedDevices([]); setSelectAll(false); fetchDevices(); } catch (error) { if (error.errorFields) { return; } message.error('批量状态变更失败'); console.error('批量状态变更失败:', error); } finally { setBatchStatusLoading(false); } }; // 打开批量移动模态框 const showBatchMoveModal = () => { if (selectedDevices.length === 0) { message.warning('请先选择要移动的设备'); return; } batchMoveForm.resetFields(); setBatchMoveModalVisible(true); }; // 执行批量移动 const handleBatchMove = async () => { try { const values = await batchMoveForm.validateFields(); setBatchMoveLoading(true); const response = await axios.put('/api/devices/batch-move', { deviceIds: selectedDevices, targetRackId: values.targetRackId, startPosition: values.startPosition }); message.success(response.data.message || '批量移动成功'); setBatchMoveModalVisible(false); setSelectedDevices([]); setSelectAll(false); fetchDevices(); fetchRacks(); } catch (error) { if (error.errorFields) { return; } message.error('批量移动失败'); console.error('批量移动失败:', error); } finally { setBatchMoveLoading(false); } }; // 打开导出选项模态框 const showExportModal = () => { if (selectedDevices.length === 0) { message.warning('请先选择要导出的设备'); return; } setExportFormat('csv'); setExportFields(deviceFields.filter(f => f.visible && f.fieldName !== 'rackId').map(f => f.fieldName)); setExportModalVisible(true); }; // 执行增强导出 const handleEnhancedExport = async () => { try { setExportLoading(true); const fieldLabels = {}; deviceFields.forEach(field => { fieldLabels[field.fieldName] = field.displayName; }); let deviceIds = []; if (exportScope === 'selected') { deviceIds = selectedDevices; } else if (exportScope === 'currentPage') { deviceIds = filteredDevicesMemo.map(device => device.deviceId); } else if (exportScope === 'all') { deviceIds = allDevices.map(device => device.deviceId); } if (deviceIds.length === 0) { message.warning('没有可导出的设备'); setExportLoading(false); return; } const params = new URLSearchParams(); deviceIds.forEach(id => params.append('deviceIds', id)); params.append('format', exportFormat); params.append('fields', JSON.stringify(exportFields)); params.append('fieldLabels', JSON.stringify(fieldLabels)); const response = await axios.get(`/api/devices/enhanced-export?${params.toString()}`, { responseType: 'blob' }); const contentType = exportFormat === 'csv' ? 'text/csv; charset=gbk' : 'application/json'; const blob = new Blob([response.data], { type: contentType }); const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `devices_export_${new Date().toISOString().split('T')[0]}.${exportFormat}`; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); message.success(`成功导出 ${deviceIds.length} 个设备`); setExportModalVisible(false); } catch (error) { message.error('导出失败'); console.error('增强导出失败:', error); } finally { setExportLoading(false); } }; // 切换选择全部设备 const handleSelectAll = () => { if (selectAll) { setSelectedDevices([]); setSelectAll(false); } else { const allIds = filteredDevicesMemo.map(device => device.deviceId); setSelectedDevices(allIds); setSelectAll(true); } }; // 处理选择变化 const handleSelectionChange = (selectedRowKeys) => { setSelectedDevices(selectedRowKeys); setSelectAll(selectedRowKeys.length === filteredDevicesMemo.length && filteredDevicesMemo.length > 0); }; // 全选复选框的处理函数 const handleSelectAllCheckbox = (e) => { const checked = e.target.checked; if (checked) { const allIds = filteredDevicesMemo.map(device => device.deviceId); setSelectedDevices(allIds); setSelectAll(true); } else { setSelectedDevices([]); setSelectAll(false); } }; const handleImport = async (file) => { try { setIsImporting(true); setImportProgress(0); setImportPhase('正在上传文件...'); setImportResult(null); const formData = new FormData(); formData.append('csvFile', file); const response = await axios.post('/api/devices/import', formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (progressEvent) => { const progress = Math.round((progressEvent.loaded * 50) / progressEvent.total); setImportProgress(Math.min(progress, 50)); setImportPhase('正在上传文件...'); } }); setImportProgress(60); setImportPhase('正在处理数据...'); setTimeout(() => { setImportProgress(80); setImportPhase('正在验证数据...'); }, 200); setTimeout(() => { setImportProgress(90); setImportPhase('正在保存数据...'); }, 400); setImportResult(response.data); setImportProgress(100); setImportPhase('导入完成'); setIsImporting(false); const { success, failed } = response.data.statistics; if (failed > 0) { message.warning(`导入完成,但有 ${failed} 条记录导入失败`); } else { message.success('所有记录导入成功'); } setTimeout(() => { fetchDevices(); }, 1000); } catch (error) { setIsImporting(false); setImportProgress(0); let errorMessage = '导入失败'; let errorDetails = []; if (error.response) { const { data } = error.response; if (data && data.errors && Array.isArray(data.errors)) { errorDetails = data.errors.map((err, index) => ({ row: err.row || index + 1, error: err.error || err.message || '未知错误' })); errorMessage = `导入失败,共发现 ${errorDetails.length} 处数据错误`; } else if (data && data.message) { errorMessage = data.message; } } else if (error.message) { errorMessage = error.message; } setImportResult({ success: false, statistics: { total: 0, success: 0, failed: 0, errors: errorDetails, message: errorMessage } }); message.error(errorMessage); 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) => { setColumnWidths(prev => ({ ...prev, [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, minWidth: 80, maxWidth: field.fieldType === 'textarea' ? 300 : 200, onHeaderCell: handleHeaderCellResize(field.fieldName), ellipsis: field.fieldType !== 'textarea', }; // 设备名称和ID列添加点击效果 if (field.fieldName === 'deviceId' || field.fieldName === 'name') { columnConfig.render = (value, record) => ( handleShowDetail(record)} style={{ color: '#1890ff', textDecoration: 'none', cursor: 'pointer', display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', }} 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 || 80, minWidth: 60, maxWidth: 100, 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('字段配置已重置为默认值'); }; const pageHeaderStyle = { marginBottom: '24px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '12px' }; const titleStyle = { fontSize: '24px', fontWeight: '700', margin: 0, background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }; const cardStyle = { borderRadius: '16px', border: 'none', boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)', overflow: 'hidden' }; const primaryButtonStyle = { height: '40px', borderRadius: '8px', background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', border: 'none', boxShadow: '0 4px 12px rgba(102, 126, 234, 0.35)', fontWeight: '500', transition: 'all 0.3s ease' }; const secondaryButtonStyle = { height: '40px', borderRadius: '8px', border: '1px solid #e8e8e8', transition: 'all 0.3s ease' }; const searchCardStyle = { borderRadius: '12px', border: '1px solid #f0f0f0', background: 'linear-gradient(180deg, #fafafa 0%, #ffffff 100%)', marginBottom: '20px' }; const modalHeaderStyle = { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '18px', fontWeight: '600' }; return (

设备管理

} style={{ width: 320, borderRadius: '8px', border: '1px solid #d9d9d9' }} value={keyword} onChange={(e) => setKeyword(e.target.value)} />
{(!loading && filteredDevicesMemo.length === 0 && !searching) && (

暂无设备数据

)} {filteredDevicesMemo.length > 0 && (
0} rowSelection={{ selectedRowKeys: selectedDevices, onChange: handleSelectionChange, columnWidth: 48, fixed: 'left', type: 'checkbox', crossPageSelect: true, selections: [ { key: 'all', text: '全选', onSelect: () => { const allIds = filteredDevicesMemo.map(device => device.deviceId); setSelectedDevices(allIds); setSelectAll(true); }}, { key: 'invert', text: '反选', onSelect: () => { const visibleIds = filteredDevicesMemo.map(device => device.deviceId); const newSelected = visibleIds.filter(id => !selectedDevices.includes(id)); setSelectedDevices(newSelected); setSelectAll(newSelected.length === filteredDevicesMemo.length); }}, { key: 'none', text: '清除选择', onSelect: () => { setSelectedDevices([]); setSelectAll(false); }}, ], }} onRow={(record) => ({ onClick: () => handleSelectionChange( selectedDevices.includes(record.deviceId) ? selectedDevices.filter(id => id !== record.deviceId) : [...selectedDevices, record.deviceId] ), })} rowClassName={(record, index) => { if (selectedDevices.includes(record.deviceId)) { return 'ant-table-row-selected'; } return index % 2 === 0 ? 'ant-table-row-even' : 'ant-table-row-odd'; }} /> )} {editingDevice ? : } {editingDevice ? '编辑设备' : '添加设备'} } open={modalVisible} onCancel={handleCancel} footer={null} width={700} style={{ borderRadius: '16px' }} styles={{ header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }, body: { padding: '24px' } }} >
{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} ); })}
字段配置 } open={fieldConfigModalVisible} onCancel={() => setFieldConfigModalVisible(false)} footer={null} width={600} styles={{ header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }, body: { padding: '24px' } }} >
({ ...acc, [field.fieldName]: field.visible }), {})} >
{deviceFields.map(field => ( {field.displayName} ))}
导入设备 } open={importModalVisible} onCancel={() => { setImportModalVisible(false); setImportProgress(0); setImportPhase(''); setImportResult(null); setIsImporting(false); }} footer={null} width={650} destroyOnHidden styles={{ header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }, body: { padding: '24px' } }} > {!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模板文件
) : isImporting ? (

正在导入设备数据

{importPhase}

`${importProgress}%`} />
) : 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; const hasFailedRecords = importResult.statistics?.failed > 0; return (hasErrors || hasFailedRecords) && (

失败记录详情:

{hasErrors ? (
{errors.map((item, index) => ( ))}
行号 失败原因
{item.row || index + 1} {item.error || '未知错误'}
) : (
检测到 {importResult.statistics.failed} 条导入失败记录,但未提供详细错误信息
)}
); })()}
) : null} 设备详情
} open={detailModalVisible} onCancel={() => { setDetailModalVisible(false); setSelectedDevice(null); }} footer={[ , ]} width={800} destroyOnHidden styles={{ header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }, body: { padding: '24px' } }} > {selectedDevice && (
基本信息 } style={{ borderRadius: '12px', border: '1px solid #f0f0f0' }} >
{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}
)}
)} 批量状态变更 } open={batchStatusModalVisible} onCancel={() => setBatchStatusModalVisible(false)} footer={[ , ]} destroyOnHidden styles={{ header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }, body: { padding: '24px' } }} >
已选择 {selectedDevices.length} 个设备
批量移动设备 } open={batchMoveModalVisible} onCancel={() => setBatchMoveModalVisible(false)} footer={[ , ]} destroyOnHidden styles={{ header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }, body: { padding: '24px' } }} >
已选择 {selectedDevices.length} 个设备
导出设备数据 } open={exportModalVisible} onCancel={() => setExportModalVisible(false)} footer={[ , ]} destroyOnHidden styles={{ header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }, body: { padding: '24px' } }} width={600} >
{deviceFields.filter(f => f.visible && f.fieldName !== 'rackId').map(field => (
{ if (e.target.checked) { setExportFields([...exportFields, field.fieldName]); } else { setExportFields(exportFields.filter(f => f !== field.fieldName)); } }} > {field.displayName}
))}
已选择 {selectedDevices.length} 个设备, 将导出 {exportFields.length} 个字段
); } export default DeviceManagement;