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,
Tag,
Row,
Col,
} 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';
import { useNavigate } from 'react-router-dom';
import { designTokens } from '../config/theme';
import {
PAGINATION_CONFIG,
DEBOUNCE_DELAY,
TABLE_SCROLL_CONFIG,
DEFAULT_DEVICE_FIELDS,
BASE_FIELD_NAMES,
SYSTEM_FIELDS,
FIXED_FIELDS,
IMPORT_CONFIG,
EXPORT_CONFIG,
MODAL_CONFIG,
DEVICE_TYPE_OPTIONS,
DEVICE_STATUS_OPTIONS,
COLUMN_WIDTH_CONFIG,
EMPTY_STATE_CONFIG,
STATUS_MAP,
TYPE_MAP,
} from '../constants/deviceManagementConstants';
import {
pageContainerStyle,
headerStyle,
titleRowStyle,
titleSectionStyle,
titleIconStyle,
titleTextStyle,
pageTitleStyle,
pageSubtitleStyle,
primaryActionStyle,
secondaryActionStyle,
statsRowStyle,
statCardStyle,
statValueStyle,
statLabelStyle,
statCardRunningStyle,
statCardMaintenanceStyle,
statCardFaultStyle,
cardStyle,
filterCardStyle,
modalHeaderStyle,
tableStyles,
searchInputStyle,
selectStyle,
refreshButtonStyle,
searchButtonStyle,
resetButtonStyle,
importModalStyles,
detailModalStyles,
exportModalStyles,
generateGlobalStyles,
emptyStateStyle,
emptyStateIconStyle,
tableContainerStyle,
resizableTitleStyles,
} from '../styles/deviceManagementStyles';
const { Option } = Select;
const { RangePicker } = DatePicker;
// 防抖 Hook
function useDebounce(value, delay = DEBOUNCE_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 = DEFAULT_DEVICE_FIELDS;
// 简单的可调整列宽的表头组件
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 navigate = useNavigate();
const [devices, setDevices] = useState([]);
const [allDevices, setAllDevices] = useState([]);
const [racks, setRacks] = useState([]);
const [rooms, setRooms] = useState([]);
const [selectedRoomId, setSelectedRoomId] = useState(null);
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: PAGINATION_CONFIG.defaultPageSize,
total: 0,
pageSizeOptions: PAGINATION_CONFIG.pageSizeOptions,
showSizeChanger: PAGINATION_CONFIG.showSizeChanger,
showTotal: PAGINATION_CONFIG.showTotal,
});
// 设备字段配置
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, DEBOUNCE_DELAY);
// 使用 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: 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: false,
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 fetchRacks = async () => {
try {
const response = await axios.get('/api/racks', {
params: { pageSize: 1000 }
});
setRacks(response.data.racks || []);
} catch (error) {
message.error('获取机柜列表失败');
console.error('获取机柜列表失败:', error);
}
};
// 获取所有机房
const fetchRooms = async () => {
try {
const response = await axios.get('/api/rooms');
setRooms(response.data || []);
} catch (error) {
message.error('获取机房列表失败');
console.error('获取机房列表失败:', error);
}
};
useEffect(() => {
fetchDevices(1, pagination.pageSize);
fetchRacks();
fetchRooms();
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);
// 编辑设备时,根据 rackId 找到对应的机房并设置 selectedRoomId
if (device.rackId) {
const rack = racks.find(r => r.rackId === device.rackId);
if (rack) {
setSelectedRoomId(rack.roomId);
}
}
} else {
form.resetFields();
setSelectedRoomId(null);
}
setModalVisible(true);
};
// 关闭模态框
const handleCancel = () => {
setModalVisible(false);
setEditingDevice(null);
setSelectedRoomId(null);
};
// 提交表单
const handleSubmit = async values => {
try {
const fixedFields = [
'deviceId',
'name',
'type',
'model',
'serialNumber',
'rackId',
'position',
'height',
'powerConsumption',
'status',
'purchaseDate',
'warrantyExpiry',
'ipAddress',
'description',
'roomId',
];
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') {
deviceData.customFields[key] = deviceData[key];
delete deviceData[key];
}
});
delete deviceData.roomId;
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) {
const errorMsg = error.response?.data?.error || error.message || '未知错误';
message.error(editingDevice ? `设备更新失败: ${errorMsg}` : `设备创建失败: ${errorMsg}`);
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 handleDeleteAll = async () => {
Modal.confirm({
title: '危险操作确认',
content:
'确定要删除所有设备吗?此操作将清空所有设备数据,包括相关的接线、网卡、端口等信息,且不可恢复!',
okText: '确认删除所有',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
const response = await axios.delete('/api/devices/delete-all');
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 handleViewDeviceTickets = device => {
navigate(`/tickets?deviceId=${device.deviceId}&deviceName=${encodeURIComponent(device.name)}&serialNumber=${encodeURIComponent(device.serialNumber || '')}&view=true`);
};
// 为设备创建工单
const handleCreateTicketForDevice = device => {
navigate(`/tickets?deviceId=${device.deviceId}&deviceName=${encodeURIComponent(device.name)}&serialNumber=${encodeURIComponent(device.serialNumber || '')}&create=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 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)}
{TYPE_MAP[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 => (
{STATUS_MAP[s]?.text || s}
))}
);
}
return (
{STATUS_MAP[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 => ({
fieldId: field.fieldId,
fieldName: field.fieldName,
displayName: field.displayName,
visible: values[`visible_${field.fieldName}`] ?? field.visible,
required: values[`required_${field.fieldName}`] ?? field.required,
}));
const response = await axios.post('/api/deviceFields/config', updatedFields);
setDeviceFields(response.data);
message.success('字段配置保存成功');
setFieldConfigModalVisible(false);
} catch (error) {
message.error('字段配置保存失败');
console.error('保存字段配置失败:', error);
}
};
// 重置字段配置为默认值
const handleResetFieldConfig = () => {
const defaultFields = defaultDeviceFields;
setDeviceFields(defaultFields);
message.success('字段配置已重置为默认值');
};
// 样式已从外部样式文件导入,无需在组件内定义
return (
}
onClick={() => showModal()}
>
添加设备
}
onClick={() => setImportModalVisible(true)}
>
导入
}
onClick={showExportModal}
>
导出
}
onClick={() => setFieldConfigModalVisible(true)}
>
字段配置
}
disabled={selectedDevices.length === 0}
onClick={showBatchStatusModal}
>
状态变更 ({selectedDevices.length})
}
disabled={selectedDevices.length === 0}
onClick={handleBatchDelete}
>
批量删除 ({selectedDevices.length})
}
onClick={handleDeleteAll}
>
删除所有
}
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)}
/>
}
htmlType="submit"
loading={searching}
>
搜索
}
onClick={() => fetchDevices(1, pagination.pageSize, true)}
style={{
borderRadius: designTokens.borderRadius.medium,
border: `1px solid ${designTokens.colors.border.light}`,
height: '36px',
}}
>
刷新
{!loading && filteredDevicesMemo.length === 0 && !searching && (
)}
{filteredDevicesMemo.length > 0 && (
`共 ${total} 条记录`,
style: { marginTop: '16px' },
}}
onChange={handleTableChange}
scroll={{ y: 500, 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 24px',
position: 'relative',
},
body: { padding: '24px' },
}}
className="device-modal"
>
{field.displayName}
{field.required && (
*
)}
}
rules={
field.required
? [{ required: true, message: `请输入${field.displayName}` }]
: []
}
>
{control}
);
} else {
// 其他字段两列布局
formItems.push(
{field.displayName}
{field.required && (
*
)}
}
rules={
field.required
? [{ required: true, message: `请输入${field.displayName}` }]
: []
}
>
{control}
);
}
});
return {formItems}
;
})()}
{/* 底部按钮区域 */}
字段配置
}
open={fieldConfigModalVisible}
onCancel={() => setFieldConfigModalVisible(false)}
footer={null}
width={600}
styles={{
header: {
borderBottom: '1px solid #f0f0f0',
padding: '16px 24px',
position: 'relative',
},
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',
position: 'relative',
},
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)
}
block
style={{
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',
}}
>
选择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={700}
destroyOnHidden
styles={{
header: {
borderBottom: '1px solid #f0f0f0',
padding: '16px 24px',
position: 'relative',
},
body: { padding: '0', overflow: 'auto' },
}}
>
{selectedDevice && (
{/* 头部信息区域 */}
{getDeviceTypeIcon(selectedDevice.type)}
{selectedDevice.name}
{getTypeLabel(selectedDevice.type)}
|
{selectedDevice.deviceId}
|
{selectedDevice.status ? getStatusConfig(selectedDevice.status).text : '-'}
{/* 内容区域 */}
{/* 基本信息卡片 */}
基本信息}
style={{ marginBottom: '16px', borderRadius: '8px' }}
>
设备型号
{selectedDevice.model || '-'}
序列号
{selectedDevice.serialNumber || '-'}
IP地址
{selectedDevice.ipAddress || '-'}
所在机房
{selectedDevice.Rack?.Room?.name || '-'}
所在机柜
{selectedDevice.Rack?.name || '-'}
位置(U)
U{selectedDevice.position || '-'}
高度
{selectedDevice.height ? `${selectedDevice.height}U` : '-'}
功率
{selectedDevice.power ? `${selectedDevice.power}W` : '-'}
状态
{selectedDevice.status ? getStatusConfig(selectedDevice.status).text : '-'}
{/* 维保信息卡片 */}
维保信息}
style={{ marginBottom: '16px', borderRadius: '8px' }}
>
购买日期
{selectedDevice.purchaseDate
? new Date(selectedDevice.purchaseDate).toLocaleDateString('zh-CN')
: '-'}
保修到期
{selectedDevice.warrantyExpiry
? new Date(selectedDevice.warrantyExpiry).toLocaleDateString('zh-CN')
: '-'}
{/* 描述信息 */}
{selectedDevice.description && (
描述}
style={{ marginBottom: '16px', borderRadius: '8px' }}
>
{selectedDevice.description}
)}
{/* 自定义字段卡片 */}
{selectedDevice.customFields && Object.keys(selectedDevice.customFields).length > 0 && (
自定义字段}
style={{ borderRadius: '8px' }}
>
{Object.entries(selectedDevice.customFields).map(([key, value]) => {
// 从 deviceFields 中查找对应的中文显示名称
const fieldConfig = deviceFields.find(f => f.fieldName === key);
const displayName = fieldConfig?.displayName || key;
return (
{displayName}
{String(value)}
);
})}
)}
)}
批量状态变更
}
open={batchStatusModalVisible}
onCancel={() => setBatchStatusModalVisible(false)}
footer={[
,
,
]}
destroyOnHidden
styles={{
header: {
borderBottom: '1px solid #f0f0f0',
padding: '16px 24px',
position: 'relative',
},
body: { padding: '24px' },
}}
>
已选择{' '}
{selectedDevices.length}{' '}
个设备
导出设备数据
}
open={exportModalVisible}
onCancel={() => setExportModalVisible(false)}
footer={[
,
,
]}
destroyOnHidden
styles={{
header: {
borderBottom: '1px solid #f0f0f0',
padding: '16px 24px',
position: 'relative',
},
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;