import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import {
Table,
Button,
Modal,
Form,
Input,
Select,
message,
Card,
Space,
Tooltip,
Tag,
Row,
Col,
Spin,
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
SearchOutlined,
UploadOutlined,
ExportOutlined,
SettingOutlined,
CloudServerOutlined,
ReloadOutlined,
AppstoreOutlined,
ToolOutlined,
EnvironmentOutlined,
FilterOutlined,
UnorderedListOutlined,
} from '@ant-design/icons';
import axios from 'axios';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { designTokens } from '../config/theme';
import {
PAGINATION_CONFIG,
DEBOUNCE_DELAY,
DEFAULT_DEVICE_FIELDS,
STATUS_MAP,
TYPE_MAP,
} from '../constants/deviceManagementConstants';
import {
pageContainerStyle,
headerStyle,
titleRowStyle,
titleSectionStyle,
titleIconStyle,
titleTextStyle,
pageTitleStyle,
pageSubtitleStyle,
secondaryActionStyle,
cardStyle,
filterCardStyle,
modalHeaderStyle,
tableContainerStyle,
emptyStateStyle,
emptyStateIconStyle,
generateGlobalStyles,
} from '../styles/deviceManagementStyles';
import {
ResizableTitle,
DeviceDetailModal,
DeviceFormModal,
ImportModal,
ExportModal,
FieldConfigModal,
BatchStatusModal,
} from '../components/device';
import { useDebounce } from '../hooks/useDebounce';
import { getDeviceTypeIcon, getStatusConfig, processDeviceData } from '../utils/deviceUtils.jsx';
const { Option } = Select;
const DEFAULT_DEVICE_FIELDS_LOCAL = DEFAULT_DEVICE_FIELDS;
function DeviceManagement() {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const [devices, setDevices] = useState([]);
const [allDevices, setAllDevices] = useState([]);
const [racks, setRacks] = useState([]);
const [rooms, setRooms] = useState([]);
const [loading, setLoading] = useState(true);
const [searching, setSearching] = useState(false);
const [keyword, setKeyword] = useState('');
const [status, setStatus] = useState('all');
const [type, setType] = useState('all');
const [roomId, setRoomId] = useState('all');
const [rackId, setRackId] = 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 [detailModalVisible, setDetailModalVisible] = useState(false);
const [selectedDevice, setSelectedDevice] = useState(null);
const [modalVisible, setModalVisible] = useState(false);
const [editingDevice, setEditingDevice] = useState(null);
const [fieldConfigModalVisible, setFieldConfigModalVisible] = useState(false);
const [batchStatusModalVisible, setBatchStatusModalVisible] = useState(false);
const [batchStatusLoading, setBatchStatusLoading] = useState(false);
const [exportModalVisible, setExportModalVisible] = useState(false);
const [selectedDevices, setSelectedDevices] = useState([]);
const [selectAll, setSelectAll] = useState(false);
const [columnWidths, setColumnWidths] = useState({});
const [advancedSearchVisible, setAdvancedSearchVisible] = useState(false);
const debouncedKeyword = useDebounce(keyword, DEBOUNCE_DELAY);
const fetchDevices = useCallback(
async (page = 1, pageSize = 50) => {
try {
setLoading(true);
const params = {
page,
pageSize,
keyword: debouncedKeyword || undefined,
status: status !== 'all' ? status : undefined,
type: type !== 'all' ? type : undefined,
roomId: roomId !== 'all' ? roomId : undefined,
rackId: rackId !== 'all' ? rackId : undefined,
};
const response = await axios.get('/api/devices', { params });
const { devices: deviceList, total } = response.data;
const processedDevices = deviceList.map(processDeviceData);
setAllDevices(processedDevices);
setPagination(prev => ({ ...prev, current: page, pageSize, total }));
} catch (error) {
message.error('获取设备列表失败');
console.error('获取设备列表失败:', error);
} finally {
setLoading(false);
}
},
[debouncedKeyword, status, type, roomId, rackId]
);
const fetchDeviceFields = async () => {
try {
setLoadingFields(true);
const response = await axios.get('/api/deviceFields');
let fields = response.data.sort((a, b) => a.order - b.order);
// 补充缺失的 options(状态和设备类型)
fields = fields.map(field => {
if (field.fieldName === 'type' && !field.options) {
const defaultTypeField = DEFAULT_DEVICE_FIELDS_LOCAL.find(f => f.fieldName === 'type');
return { ...field, options: defaultTypeField?.options || [] };
}
if (field.fieldName === 'status' && !field.options) {
const defaultStatusField = DEFAULT_DEVICE_FIELDS_LOCAL.find(
f => f.fieldName === 'status'
);
return { ...field, options: defaultStatusField?.options || [] };
}
return field;
});
setDeviceFields(fields);
} catch (error) {
message.error('获取字段配置失败');
console.error('获取字段配置失败:', error);
setDeviceFields(DEFAULT_DEVICE_FIELDS_LOCAL);
} finally {
setLoadingFields(false);
}
};
const fetchRacks = async () => {
try {
const response = await axios.get('/api/racks/all');
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.rooms || []);
} catch (error) {
message.error('获取机房列表失败');
console.error('获取机房列表失败:', error);
}
};
useEffect(() => {
fetchDevices(1, pagination.pageSize);
fetchRacks();
fetchRooms();
fetchDeviceFields();
}, [fetchDevices]);
useEffect(() => {
const deviceIdFromUrl = searchParams.get('deviceId');
if (deviceIdFromUrl) {
const fetchDeviceDetail = async () => {
try {
const response = await axios.get(`/api/devices/${deviceIdFromUrl}`);
if (response.data) {
setSelectedDevice(response.data);
setDetailModalVisible(true);
setSearchParams({});
}
} catch (error) {
console.error('获取设备详情失败:', error);
const errorStatus = error.response?.status;
const errorMessage = error.response?.data?.error;
if (errorStatus === 404 || errorMessage === '设备不存在') {
message.warning('该设备已被删除,无法查看详情');
} else {
message.error(errorMessage || '获取设备详情失败');
}
setSearchParams({});
}
};
fetchDeviceDetail();
}
}, [searchParams, setSearchParams]);
const showModal = (device = null) => {
setEditingDevice(device);
setModalVisible(true);
};
const handleCancel = () => {
setModalVisible(false);
setEditingDevice(null);
};
const handleSubmit = async deviceData => {
try {
if (editingDevice) {
await axios.put(`/api/devices/${editingDevice.deviceId}`, deviceData);
message.success('设备更新成功');
} else {
await axios.post('/api/devices', deviceData);
message.success('设备创建成功');
}
setModalVisible(false);
fetchDevices(1, pagination.pageSize);
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');
setRoomId(values.roomId || 'all');
setRackId(values.rackId || 'all');
setPagination(prev => ({ ...prev, current: 1 }));
setTimeout(() => setSearching(false), 300);
};
const handleReset = () => {
setSearching(true);
setKeyword('');
setStatus('all');
setType('all');
setRoomId('all');
setRackId('all');
searchForm.resetFields();
setTimeout(() => setSearching(false), 300);
};
const handleTableChange = newPagination => {
setPagination(newPagination);
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.post('/api/devices/batch-delete', {
deviceIds: selectedDevices,
});
message.success(response.data.message || '批量删除成功');
setSelectedDevices([]);
setSelectAll(false);
fetchDevices(1, 50);
} catch (error) {
message.error('批量删除失败');
console.error('批量删除设备失败:', error);
// 显示更具体的错误信息
if (error.response?.data?.error) {
console.error('后端错误详情:', error.response.data.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, 50);
} catch (error) {
message.error('删除所有设备失败');
console.error('删除所有设备失败:', error);
}
},
});
};
const handleBatchToIdle = async () => {
if (selectedDevices.length === 0) {
message.warning('请先选择要标记为空闲的设备');
return;
}
Modal.confirm({
title: '确认标记为空闲',
content: `确定要将选中的 ${selectedDevices.length} 个设备标记为空闲设备吗?`,
okText: '确认',
cancelText: '取消',
onOk: async () => {
try {
const response = await axios.post('/api/idle-devices/batch-from-devices', {
deviceIds: selectedDevices,
idleReason: '从设备管理批量转入',
});
message.success(response.data.message || '设备已标记为空闲');
setSelectedDevices([]);
setSelectAll(false);
fetchDevices(1, 50);
} catch (error) {
message.error(error.response?.data?.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(1, pagination.pageSize);
} 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;
}
setBatchStatusModalVisible(true);
};
const handleBatchStatusChange = async newStatus => {
setBatchStatusLoading(true);
try {
const response = await axios.put('/api/devices/batch-status', {
deviceIds: selectedDevices,
status: newStatus,
});
message.success(response.data.message || '批量状态变更成功');
setBatchStatusModalVisible(false);
setSelectedDevices([]);
setSelectAll(false);
fetchDevices(1, pagination.pageSize);
} catch (error) {
message.error('批量状态变更失败');
console.error('批量状态变更失败:', error);
} finally {
setBatchStatusLoading(false);
}
};
const showExportModal = () => {
if (selectedDevices.length === 0) {
message.warning('请先选择要导出的设备');
return;
}
setExportModalVisible(true);
};
const handleEnhancedExport = async ({ format, scope }) => {
let deviceIds = [];
if (scope === 'selected') {
deviceIds = selectedDevices;
} else if (scope === 'currentPage') {
deviceIds = allDevices.map(device => device.deviceId);
} else if (scope === 'all') {
deviceIds = allDevices.map(device => device.deviceId);
}
if (deviceIds.length === 0) {
message.warning('没有可导出的设备');
return;
}
const params = new URLSearchParams();
deviceIds.forEach(id => params.append('deviceIds', id));
params.append('format', format);
const response = await axios.get(`/api/devices/enhanced-export?${params.toString()}`, {
responseType: 'blob',
});
const contentType = format === '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]}.${format}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
message.success(`成功导出 ${deviceIds.length} 个设备`);
};
const handleImport = async (file, callbacks) => {
try {
callbacks.onProgress(0, '正在上传文件...');
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);
callbacks.onProgress(Math.min(progress, 50), '正在上传文件...');
},
});
callbacks.onProgress(60, '正在处理数据...');
setTimeout(() => {
callbacks.onProgress(80, '正在验证数据...');
}, 200);
setTimeout(() => {
callbacks.onProgress(90, '正在保存数据...');
}, 400);
callbacks.onSuccess(response.data);
const { success, failed } = response.data.statistics;
if (failed > 0) {
message.warning(`导入完成,但有 ${failed} 条记录导入失败`);
} else {
message.success('所有记录导入成功');
}
setTimeout(() => {
fetchDevices(1, pagination.pageSize);
}, 1000);
} catch (error) {
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;
}
callbacks.onError({
message: errorMessage,
errors: errorDetails,
});
message.error(errorMessage);
console.error('导入设备失败:', error);
}
};
const handleSaveFieldConfig = async updatedFields => {
try {
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 = defaultFields => {
setDeviceFields(defaultFields);
};
const handleSelectionChange = selectedRowKeys => {
setSelectedDevices(selectedRowKeys);
setSelectAll(selectedRowKeys.length === allDevices.length && allDevices.length > 0);
};
const handleHeaderCellResize = key => column => ({
width: column.width,
onResize: width => {
setColumnWidths(prev => ({ ...prev, [key]: width }));
},
});
const columns = useMemo(() => {
const generatedColumns = [];
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 => (
管理您的IT设备资产
暂无设备数据