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) => (
}
onClick={() => showModal(record)}
size="small"
style={{ color: '#1890ff', padding: '4px 8px' }}
/>
}
onClick={() => handleDelete(record.deviceId)}
size="small"
style={{ padding: '4px 8px' }}
/>
),
});
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 (
设备管理
}
onClick={() => showModal()}
>
添加设备
}
onClick={() => setImportModalVisible(true)}
>
导入设备
}
onClick={() => setFieldConfigModalVisible(true)}
>
字段配置
}
onClick={resetColumnWidths}
title="重置列宽"
>
重置列宽
}
style={{
width: 320,
borderRadius: '8px',
border: '1px solid #d9d9d9'
}}
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
}
style={primaryButtonStyle}
>
搜索
}
onClick={showExportModal}
>
增强导出
{(!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' } }}
>
{control}
);
})}
字段配置
}
open={fieldConfigModalVisible}
onCancel={() => setFieldConfigModalVisible(false)}
footer={null}
width={600}
styles={{ header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }, body: { padding: '24px' } }}
>
导入设备
}
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)
} block style={primaryButtonStyle}>
选择CSV文件
) : isImporting ? (
) : 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;