feat: 添加设备位置冲突检查功能并优化操作日志

feat(api): 在设备API中添加checkPosition接口用于检查U位冲突
feat(frontend): 在设备表单和空闲设备管理中实现U位冲突检查
refactor(backend): 重构操作日志功能,添加设备描述生成和元数据构建工具
fix(backend): 修复批量导入机柜时的ID验证规则
feat(backend): 为机柜导入添加创建和跳过机柜的详细返回信息
fix(backend): 修复设备删除时未检查关联接线的问题
feat(backend): 添加机柜导入模板生成脚本
perf(frontend): 优化机柜管理页面的导入结果展示
fix(frontend): 修复设备端口删除时的关联接线检查
This commit is contained in:
zhang1106
2026-03-26 14:35:08 +08:00
parent 0b7732e427
commit d9c46245df
14 changed files with 2152 additions and 384 deletions
+84 -1
View File
@@ -29,8 +29,10 @@ import {
InboxOutlined,
ClockCircleOutlined,
UploadOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import axios from 'axios';
import { deviceAPI } from '../api';
const { Title, Text, Paragraph } = Typography;
const { Option } = Select;
@@ -52,6 +54,8 @@ const IdleDeviceManagement = () => {
const [rooms, setRooms] = useState([]);
const [selectedRoomId, setSelectedRoomId] = useState(null);
const [selectedShelveRoomId, setSelectedShelveRoomId] = useState(null);
const [shelvePositionConflict, setShelvePositionConflict] = useState(null);
const [shelveSelectedRackId, setShelveSelectedRackId] = useState(null);
const fetchIdleDevices = useCallback(async () => {
setLoading(true);
@@ -144,6 +148,8 @@ const IdleDeviceManagement = () => {
const handleShelve = (record) => {
setShelvingDevice(record);
setShelvePositionConflict(null);
setShelveSelectedRackId(null);
let roomId = null;
if (record.rackId) {
const rack = racks.find(r => r.rackId === record.rackId);
@@ -164,12 +170,69 @@ const IdleDeviceManagement = () => {
position: record.position,
description: record.description,
});
if (record.rackId) {
setShelveSelectedRackId(record.rackId);
}
setIsShelveModalVisible(true);
};
const checkShelvePositionConflict = async (rackId, position, height) => {
if (!rackId || !position) {
setShelvePositionConflict(null);
return;
}
try {
const result = await deviceAPI.checkPosition(rackId, { position, height: height || 1 });
if (!result.available) {
setShelvePositionConflict(result.reason);
} else {
setShelvePositionConflict(null);
}
} catch (error) {
console.error('检查U位冲突失败:', error);
setShelvePositionConflict(null);
}
};
const handleShelveRackChange = (value) => {
setShelveSelectedRackId(value);
const position = shelveForm.getFieldValue('position');
const height = shelveForm.getFieldValue('height');
if (position) {
checkShelvePositionConflict(value, position, height);
} else {
setShelvePositionConflict(null);
}
};
const handleShelvePositionChange = (e) => {
const value = e.target.value ? parseInt(e.target.value) : null;
const height = shelveForm.getFieldValue('height');
if (shelveSelectedRackId && value) {
checkShelvePositionConflict(shelveSelectedRackId, value, height);
} else {
setShelvePositionConflict(null);
}
};
const handleShelveHeightChange = (e) => {
const value = e.target.value ? parseInt(e.target.value) : null;
const position = shelveForm.getFieldValue('position');
if (shelveSelectedRackId && position) {
checkShelvePositionConflict(shelveSelectedRackId, position, value);
} else {
setShelvePositionConflict(null);
}
};
const handleShelveSubmit = async () => {
try {
const values = await shelveForm.validateFields();
if (shelvePositionConflict) {
message.error('存在U位冲突,请重新选择上架位置');
return;
}
const submitData = {
name: values.name,
type: values.type,
@@ -866,6 +929,7 @@ const IdleDeviceManagement = () => {
placeholder={selectedShelveRoomId ? "请选择机柜" : "请先选择机房"}
disabled={!selectedShelveRoomId}
style={{ borderRadius: '8px' }}
onChange={handleShelveRackChange}
>
{racks
.filter((rack) => rack.roomId === selectedShelveRoomId)
@@ -879,10 +943,29 @@ const IdleDeviceManagement = () => {
</Col>
<Col span={8}>
<Form.Item name="position" label="U位">
<Input type="number" placeholder="请输入U位" min={1} style={{ borderRadius: '8px' }} />
<Input type="number" placeholder="请输入U位" min={1} style={{ borderRadius: '8px' }} onChange={handleShelvePositionChange} />
</Form.Item>
</Col>
</Row>
{shelvePositionConflict && (
<div style={{ marginTop: '12px' }}>
<div style={{
background: '#fef2f2',
border: '1px solid #fecaca',
borderRadius: '8px',
padding: '12px 16px',
display: 'flex',
alignItems: 'flex-start',
gap: '10px'
}}>
<ExclamationCircleOutlined style={{ color: '#ef4444', fontSize: '18px', marginTop: '2px' }} />
<div>
<div style={{ color: '#dc2626', fontWeight: 600, marginBottom: '4px' }}>U位冲突</div>
<div style={{ color: '#991b1b', fontSize: '13px' }}>{shelvePositionConflict}</div>
</div>
</div>
</div>
)}
</div>
<div style={{
File diff suppressed because it is too large Load Diff
@@ -1093,15 +1093,8 @@ const Rack3DVisualization = () => {
device={selectedDevice}
onClose={() => setSelectedDevice(null)}
onEdit={handleEditDevice}
onAddNic={handleAddNic}
onAddPort={handleAddPort}
onAddCable={handleAddCable}
tooltipFields={tooltipFields}
cables={deviceCables}
onRefreshCables={() =>
selectedDevice && fetchDeviceCables(selectedDevice.deviceId || selectedDevice.id)
}
onDeleteCable={handleDeleteCable}
refreshTrigger={refreshTrigger}
/>
</Content>
+104 -9
View File
@@ -417,7 +417,8 @@ function RackManagement() {
message.success('机柜删除成功');
fetchRacks();
} catch (error) {
message.error('机柜删除失败');
const errorMsg = error.response?.data?.error || '机柜删除失败';
message.error(errorMsg);
console.error('机柜删除失败:', error);
}
},
@@ -438,8 +439,16 @@ function RackManagement() {
cancelText: '取消',
onOk: async () => {
try {
await Promise.all(selectedRackIds.map(id => axios.delete(`/api/racks/${id}`)));
message.success(`成功删除 ${selectedRackIds.length} 个机柜`);
const results = await Promise.allSettled(selectedRackIds.map(id => axios.delete(`/api/racks/${id}`)));
const succeeded = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected');
if (succeeded > 0) {
message.success(`成功删除 ${succeeded} 个机柜`);
}
if (failed.length > 0) {
const firstError = failed[0].reason.response?.data?.error || '部分机柜删除失败';
message.error(`${firstError}${failed.length} 个失败)`);
}
setSelectedRackIds([]);
fetchRacks();
} catch (error) {
@@ -519,13 +528,33 @@ function RackManagement() {
setImportProgress(100);
setImportPhase('导入完成');
setImportResult(response.data);
const resData = response.data;
const importResult = {
total: resData.total || 0,
successCount: resData.imported || 0,
duplicates: resData.duplicates || 0,
failedCount: 0,
errors: [],
createdRacks: resData.createdRacks || [],
skippedRacks: resData.skippedRacks || []
};
if (resData.details && Array.isArray(resData.details)) {
importResult.failedCount = resData.details.length;
importResult.errors = resData.details.map(item => ({
row: item.row,
error: item.errors.join('')
}));
}
setImportResult(importResult);
setIsImporting(false);
if (response.data.success) {
if (resData.success) {
message.success('机柜导入成功');
} else {
message.warning(response.data.message || '部分记录导入失败');
message.warning(resData.message || '部分记录导入失败');
}
fetchRacks();
@@ -533,8 +562,24 @@ function RackManagement() {
} catch (error) {
setIsImporting(false);
setImportProgress(0);
message.error('机柜导入失败');
console.error('机柜导入失败:', error);
const errorData = error.response?.data;
if (errorData?.details && Array.isArray(errorData.details)) {
const importResult = {
total: errorData.total || 0,
successCount: 0,
failedCount: errorData.details.length,
errors: errorData.details.map(item => ({
row: item.row,
error: item.errors.join('')
}))
};
setImportResult(importResult);
setImportPhase('导入失败');
} else {
message.error(errorData?.error || errorData?.message || '机柜导入失败');
console.error('机柜导入失败:', error);
}
return false;
}
},
@@ -1380,10 +1425,60 @@ function RackManagement() {
导入失败{importResult.failedCount}
</p>
)}
{importResult.duplicates > 0 && (
<p style={{ margin: '8px 0', color: '#faad14' }}>
跳过已存在{importResult.duplicates}
</p>
)}
</div>
{importResult.createdRacks && importResult.createdRacks.length > 0 && (
<div style={{ marginBottom: '20px' }}>
<p style={{ fontWeight: '600', marginBottom: '8px', color: '#52c41a' }}>
本次新增机柜{importResult.createdRacks.length}
</p>
<div style={{
maxHeight: '150px',
overflow: 'auto',
background: '#f6ffed',
border: '1px solid #b7eb8f',
borderRadius: '8px',
padding: '12px'
}}>
{importResult.createdRacks.map((rack, idx) => (
<div key={idx} style={{ fontSize: '13px', marginBottom: '4px' }}>
{rack.rackId} - {rack.name}
</div>
))}
</div>
</div>
)}
{importResult.skippedRacks && importResult.skippedRacks.length > 0 && (
<div style={{ marginBottom: '20px' }}>
<p style={{ fontWeight: '600', marginBottom: '8px', color: '#faad14' }}>
已跳过机柜{importResult.skippedRacks.length}
</p>
<div style={{
maxHeight: '150px',
overflow: 'auto',
background: '#fffbe6',
border: '1px solid #ffe58f',
borderRadius: '8px',
padding: '12px'
}}>
{importResult.skippedRacks.map((rack, idx) => (
<div key={idx} style={{ fontSize: '13px', marginBottom: '4px' }}>
{rack.rackId} - {rack.name}
</div>
))}
</div>
</div>
)}
{importResult.errors && importResult.errors.length > 0 && (
<div style={{ marginBottom: '20px' }}>
<p style={{ fontWeight: '600', marginBottom: '8px' }}>错误详情</p>
<p style={{ fontWeight: '600', marginBottom: '8px', color: '#ff4d4f' }}>错误详情</p>
{importResult.errors.slice(0, 5).map((err, idx) => (
<div
key={idx}