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, SafetyOutlined, DatabaseOutlined, AppstoreOutlined, MoreOutlined, ReloadOutlined, ExportOutlined, FileExcelOutlined, SwapOutlined } from '@ant-design/icons'; import axios from 'axios'; import dayjs from 'dayjs'; const { Option } = Select; const { RangePicker } = DatePicker; const designTokens = { colors: { primary: { main: '#667eea', gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', light: '#8b9ff0', dark: '#4f5db8' }, success: { main: '#10b981', gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', light: '#34d399', dark: '#047857' }, warning: { main: '#f59e0b', gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)', light: '#fbbf24', dark: '#b45309' }, error: { main: '#ef4444', gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)', light: '#f87171', dark: '#b91c1c' }, text: { primary: '#1e293b', secondary: '#64748b', tertiary: '#94a3b8', inverse: '#ffffff' }, background: { primary: '#ffffff', secondary: '#f8fafc', tertiary: '#f1f5f9', dark: '#1e293b' }, border: { light: '#e2e8f0', medium: '#cbd5e1', dark: '#94a3b8' }, device: { server: '#3b82f6', switch: '#22c55e', router: '#f59e0b', storage: '#8b5cf6', other: '#64748b' }, status: { normal: '#10b981', running: '#10b981', warning: '#f59e0b', error: '#ef4444', fault: '#ef4444', offline: '#6b7280', maintenance: '#3b82f6' } }, shadows: { small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)', large: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)', xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)', glow: '0 0 20px rgba(102, 126, 234, 0.3)' }, borderRadius: { small: '6px', medium: '10px', large: '16px', xl: '24px', round: '50%' }, transitions: { fast: '150ms cubic-bezier(0.4, 0, 0.2, 1)', normal: '300ms cubic-bezier(0.4, 0, 0.2, 1)', slow: '500ms cubic-bezier(0.4, 0, 0.2, 1)' }, spacing: { xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px' } }; // 防抖 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: false, order: 1, visible: false }, { 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: false, order: 11, visible: true }, { fieldName: 'warrantyExpiry', displayName: '保修到期', fieldType: 'date', required: false, 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 [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 debouncedKeyword = useDebounce(keyword, 300); // 使用 useMemo 缓存筛选后的设备数据(现在直接使用 allDevices,因为后端已经处理了筛选) const filteredDevicesMemo = useMemo(() => { return allDevices; }, [allDevices]); // 获取所有设备(支持搜索、筛选和分页) const fetchDevices = useCallback(async (page = 1, pageSize = 10, forceRefresh = false) => { try { setLoading(true); // 使用后端分页加载 const params = { page, pageSize, keyword: debouncedKeyword || undefined, status: status !== 'all' ? status : undefined, type: type !== 'all' ? type : undefined }; const response = await axios.get('/api/devices', { params }); const { devices, total } = response.data; // 处理设备数据,展开自定义字段 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; }); setAllDevices(processedDevices); setPagination(prev => ({ ...prev, current: page, pageSize, total })); } catch (error) { message.error('获取设备列表失败'); console.error('获取设备列表失败:', error); } finally { setLoading(false); } }, [debouncedKeyword, status, type]); // 获取设备字段配置 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(1, pagination.pageSize); fetchRacks(); fetchDeviceFields(); }, [fetchDevices]); // 同步当前页设备数据 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 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 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, status } = error.response; if (status === 500 && data && data.errors && Array.isArray(data.errors) && data.errors.length > 0) { errorDetails = data.errors.map((err) => ({ row: err.row || 0, error: err.error || err.message || '服务器内部错误' })); errorMessage = `导入失败:${errorDetails[0].error}`; } else 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 || data.error)) { errorMessage = data.message || data.error; } } else if (error.message) { errorMessage = error.message; } setImportResult({ success: false, statistics: { total: 0, success: 0, failed: errorDetails.length > 0 ? errorDetails.length : 1, errors: errorDetails.length > 0 ? errorDetails : [{ row: 0, error: errorMessage }], 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 pageContainerStyle = { minHeight: '100vh', background: designTokens.colors.background.secondary, padding: designTokens.spacing.lg }; const headerStyle = { marginBottom: designTokens.spacing.lg }; const titleRowStyle = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: designTokens.spacing.lg, flexWrap: 'wrap', gap: designTokens.spacing.md }; const titleSectionStyle = { display: 'flex', alignItems: 'center', gap: designTokens.spacing.md }; const titleIconStyle = { width: '44px', height: '44px', borderRadius: designTokens.borderRadius.medium, background: designTokens.colors.primary.gradient, display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: designTokens.shadows.medium }; const titleTextStyle = { display: 'flex', flexDirection: 'column', gap: '2px' }; const pageTitleStyle = { fontSize: '22px', fontWeight: '700', margin: 0, color: designTokens.colors.text.primary, lineHeight: 1.2 }; const pageSubtitleStyle = { fontSize: '13px', color: designTokens.colors.text.secondary, margin: 0 }; const actionButtonStyle = { height: '36px', borderRadius: designTokens.borderRadius.small, fontSize: '13px', fontWeight: '500', display: 'inline-flex', alignItems: 'center', gap: '6px' }; const primaryActionStyle = { ...actionButtonStyle, background: designTokens.colors.primary.gradient, border: 'none', color: '#ffffff !important', boxShadow: designTokens.shadows.small }; const secondaryActionStyle = { ...actionButtonStyle, background: designTokens.colors.background.primary, border: `1px solid ${designTokens.colors.border.light}`, color: designTokens.colors.text.primary }; const dangerActionStyle = { ...actionButtonStyle, background: designTokens.colors.error.main, border: 'none', color: '#ffffff' }; const primaryButtonStyle = { height: '40px', borderRadius: designTokens.borderRadius.small, background: designTokens.colors.primary.gradient, border: 'none', color: '#ffffff', boxShadow: designTokens.shadows.small, fontWeight: '500', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }; const statsRowStyle = { display: 'flex', gap: designTokens.spacing.md, marginBottom: designTokens.spacing.lg, flexWrap: 'wrap' }; const statCardStyle = { flex: 1, minWidth: '140px', maxWidth: '200px', padding: `${designTokens.spacing.md}px ${designTokens.spacing.lg}px`, background: designTokens.colors.background.primary, borderRadius: designTokens.borderRadius.medium, border: `1px solid ${designTokens.colors.border.light}`, boxShadow: designTokens.shadows.small, transition: `all ${designTokens.transitions.fast}` }; const statValueStyle = { fontSize: '24px', fontWeight: '700', color: designTokens.colors.text.primary, lineHeight: 1.2 }; const statLabelStyle = { fontSize: '12px', color: designTokens.colors.text.secondary, marginTop: '4px' }; const statCardRunningStyle = { ...statCardStyle, borderLeft: `3px solid ${designTokens.colors.success.main}`, background: `${designTokens.colors.success.main}08` }; const statCardMaintenanceStyle = { ...statCardStyle, borderLeft: `3px solid ${designTokens.colors.warning.main}`, background: `${designTokens.colors.warning.main}08` }; const statCardFaultStyle = { ...statCardStyle, borderLeft: `3px solid ${designTokens.colors.error.main}`, background: `${designTokens.colors.error.main}08` }; const cardStyle = { borderRadius: designTokens.borderRadius.large, border: 'none', boxShadow: designTokens.shadows.medium, overflow: 'hidden', background: designTokens.colors.background.primary }; const filterCardStyle = { borderRadius: designTokens.borderRadius.medium, border: 'none', boxShadow: designTokens.shadows.small, background: designTokens.colors.background.primary, marginBottom: designTokens.spacing.lg }; const modalHeaderStyle = { display: 'flex', alignItems: 'center', gap: designTokens.spacing.sm, fontSize: '18px', fontWeight: '600' }; return (

设备管理

管理您的IT设备资产

} style={{ width: '280px', borderRadius: designTokens.borderRadius.medium, border: `1px solid ${designTokens.colors.border.light}`, transition: `all ${designTokens.transitions.fast}` }} value={keyword} onChange={(e) => setKeyword(e.target.value)} />
{(!loading && filteredDevicesMemo.length === 0 && !searching) && (

暂无设备数据

)} {filteredDevicesMemo.length > 0 && (
`共 ${total} 条记录`, style: { marginTop: '16px' } }} onChange={handleTableChange} scroll={{ y: 'calc(100vh - 380px)', scrollToFirstRowOnChange: true }} virtual components={{ header: { cell: ResizableTitle, }, }} style={{ width: '100%', maxWidth: '100%' }} size="middle" showHeader={filteredDevicesMemo.length > 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 50px 16px 24px' }, body: { padding: '24px' } }} className="device-modal" >
{deviceFields.filter(field => field.fieldName !== 'deviceId').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.filter(field => field.fieldName !== 'deviceId').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文件格式要求:

{(() => { // 获取必填字段 const requiredFields = deviceFields.filter(f => f.visible && f.required); // 获取可选字段 const optionalFields = deviceFields.filter(f => f.visible && !f.required); return ( <> {requiredFields.length > 0 && (
必填字段: {requiredFields.map(f => f.displayName).join('、')}
)} {optionalFields.length > 0 && (
可选字段: {optionalFields.map(f => f.displayName).join('、')}
)} ); })()}
  • 设备类型:server(服务器)、switch(交换机)、router(路由器)、storage(存储设备)、other(其他)
  • 状态值: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={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;