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
+413 -170
View File
@@ -9,62 +9,89 @@ import {
Card,
Tooltip,
Button,
Popconfirm,
Table,
Badge,
Row,
Col,
Divider,
Collapse,
Spin,
Pagination,
} from 'antd';
import {
ApiOutlined,
CloudServerOutlined,
EnvironmentOutlined,
EditOutlined,
PlusCircleOutlined,
DeleteOutlined,
FileTextOutlined,
ToolOutlined,
EyeOutlined,
DesktopOutlined,
FieldTimeOutlined,
InfoCircleOutlined,
LinkOutlined,
FolderOutlined,
LeftOutlined,
RightOutlined,
} from '@ant-design/icons';
import NetworkCardPanel from './NetworkCardPanel';
import { deviceAPI } from '../api';
import { designTokens } from '../config/theme';
import dayjs from 'dayjs';
import api from '../api';
const { Text, Title } = Typography;
const { Panel } = Collapse;
const PAGE_SIZE = 3;
function DeviceDetailDrawer({
device,
visible,
onClose,
cables,
onRefreshCables,
onEdit,
onAddNic,
onAddPort,
onAddCable,
onDeleteCable,
tooltipFields,
refreshTrigger,
onViewTicket,
onCreateTicket,
}) {
const [activeTab, setActiveTab] = useState('ports');
const [tickets, setTickets] = useState([]);
const [ticketsLoading, setTicketsLoading] = useState(false);
const [ticketsPagination, setTicketsPagination] = useState({
current: 1,
pageSize: 5,
pageSize: PAGE_SIZE,
total: 0,
});
// 获取设备关联的工单列表
const fetchDeviceTickets = useCallback(async (page = 1, pageSize = 5) => {
const [networkCards, setNetworkCards] = useState([]);
const [networkCardsLoading, setNetworkCardsLoading] = useState(false);
const [expandedCards, setExpandedCards] = useState([]);
const [portsPage, setPortsPage] = useState(1);
const [cablesPage, setCablesPage] = useState(1);
const fetchNetworkCards = useCallback(async () => {
if (!device?.deviceId) return;
setNetworkCardsLoading(true);
try {
const response = await api.get(`/network-cards/device/${device.deviceId}/with-ports`);
const cardsData = response.data || response || [];
setNetworkCards(cardsData);
const initialExpanded = cardsData
.filter(card => card.ports && card.ports.length > 0)
.map(card => card.nicId);
setExpandedCards(initialExpanded);
setPortsPage(1);
} catch (error) {
console.error('获取网卡数据失败:', error);
setNetworkCards([]);
} finally {
setNetworkCardsLoading(false);
}
}, [device?.deviceId]);
useEffect(() => {
if (visible && device?.deviceId) {
fetchNetworkCards();
}
}, [visible, device?.deviceId, fetchNetworkCards, refreshTrigger]);
const fetchDeviceTickets = useCallback(async (page = 1, pageSize = PAGE_SIZE) => {
if (!device?.deviceId) return;
setTicketsLoading(true);
try {
@@ -75,7 +102,7 @@ function DeviceDetailDrawer({
setTickets(response.data || []);
setTicketsPagination({
current: response.page || 1,
pageSize: response.pageSize || 5,
pageSize: response.pageSize || PAGE_SIZE,
total: response.total || 0,
});
} catch (error) {
@@ -85,14 +112,12 @@ function DeviceDetailDrawer({
}
}, [device?.deviceId]);
// 当设备变化或标签页切换到工单时,加载工单数据
useEffect(() => {
if (visible && device?.deviceId && activeTab === 'tickets') {
fetchDeviceTickets(1, 5);
fetchDeviceTickets(1, PAGE_SIZE);
}
}, [visible, device?.deviceId, activeTab, fetchDeviceTickets]);
// 工单表格列定义
const ticketColumns = useMemo(() => [
{
title: '工单编号',
@@ -149,22 +174,7 @@ function DeviceDetailDrawer({
return dayjs(date).format('YYYY-MM-DD HH:mm');
},
},
{
title: '操作',
key: 'action',
width: 80,
render: (_, record) => (
<Button
type="link"
size="small"
icon={<EyeOutlined />}
onClick={() => onViewTicket?.(record)}
>
查看
</Button>
),
},
], [onViewTicket]);
], []);
const deviceCables = useMemo(() => {
if (!device || !cables) return [];
@@ -173,6 +183,18 @@ function DeviceDetailDrawer({
);
}, [device, cables]);
const paginatedCables = useMemo(() => {
const start = (cablesPage - 1) * PAGE_SIZE;
const end = start + PAGE_SIZE;
return deviceCables.slice(start, end);
}, [deviceCables, cablesPage]);
const cablesTotalPages = Math.ceil(deviceCables.length / PAGE_SIZE);
useEffect(() => {
setCablesPage(1);
}, [deviceCables.length]);
const getStatusTag = useCallback(status => {
const config = {
running: { color: 'success', text: '运行中' },
@@ -182,11 +204,26 @@ function DeviceDetailDrawer({
fault: { color: 'error', text: '故障' },
offline: { color: 'default', text: '离线' },
maintenance: { color: 'processing', text: '维护中' },
free: { color: 'success', text: '空闲' },
occupied: { color: 'processing', text: '占用' },
};
const { color, text } = config[status] || { color: 'default', text: status };
return <Tag color={color}>{text}</Tag>;
}, []);
const getPortTypeTag = useCallback(type => {
const config = {
RJ45: { color: 'blue', text: 'RJ45' },
SFP: { color: 'green', text: 'SFP' },
'SFP+': { color: 'cyan', text: 'SFP+' },
SFP28: { color: 'purple', text: 'SFP28' },
QSFP: { color: 'orange', text: 'QSFP' },
QSFP28: { color: 'red', text: 'QSFP28' },
};
const { color, text } = config[type] || { color: 'default', text: type };
return <Tag color={color}>{text}</Tag>;
}, []);
const getDeviceTypeName = useCallback(type => {
const typeMap = {
server: '服务器',
@@ -200,9 +237,331 @@ function DeviceDetailDrawer({
return typeMap[type?.toLowerCase()] || type || '未知设备';
}, []);
const renderPortTable = (ports) => {
const columns = [
{
title: '端口名称',
dataIndex: 'portName',
key: 'portName',
width: 120,
render: text => <span style={{ fontWeight: 500 }}>{text}</span>,
},
{
title: '类型',
dataIndex: 'portType',
key: 'portType',
width: 80,
render: type => getPortTypeTag(type),
},
{
title: '速率',
dataIndex: 'portSpeed',
key: 'portSpeed',
width: 70,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 70,
render: status => getStatusTag(status),
},
{
title: 'VLAN',
dataIndex: 'vlanId',
key: 'vlanId',
width: 60,
render: vlanId => vlanId || '-',
},
];
return (
<Table
columns={columns}
dataSource={ports}
rowKey="portId"
pagination={false}
size="small"
scroll={{ x: 400 }}
/>
);
};
const renderCardHeader = card => {
const stats = card.stats || { free: 0, occupied: 0, fault: 0, total: 0 };
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div
style={{
width: '36px',
height: '36px',
borderRadius: '8px',
background: card.isUngrouped
? 'linear-gradient(135deg, #94a3b8 0%, #64748b 100%)'
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
}}
>
{card.isUngrouped ? <FolderOutlined /> : <CloudServerOutlined />}
</div>
<div>
<div style={{ fontWeight: 600, fontSize: '14px', color: '#1e293b' }}>
{card.name}
{card.slotNumber && (
<span style={{ color: '#94a3b8', marginLeft: 8 }}>插槽 {card.slotNumber}</span>
)}
</div>
<div style={{ fontSize: '12px', color: '#64748b' }}>
{card.description || (card.isUngrouped ? '未分配到网卡的端口' : '网卡')}
</div>
</div>
</div>
<Space size={12}>
<Badge count={stats.free} style={{ backgroundColor: designTokens.colors.success }} />
<span style={{ fontSize: '12px', color: '#64748b' }}>空闲</span>
<Badge count={stats.occupied} style={{ backgroundColor: '#1677ff' }} />
<span style={{ fontSize: '12px', color: '#64748b' }}>占用</span>
<Badge count={stats.fault} style={{ backgroundColor: designTokens.colors.error }} />
<span style={{ fontSize: '12px', color: '#64748b' }}>故障</span>
</Space>
</div>
);
};
const paginatedNetworkCards = useMemo(() => {
const start = (portsPage - 1) * PAGE_SIZE;
const end = start + PAGE_SIZE;
return networkCards.slice(start, end);
}, [networkCards, portsPage]);
const portsTotalPages = Math.ceil(networkCards.length / PAGE_SIZE);
const renderPortsTab = () => {
if (networkCardsLoading) {
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
<Spin size="large" tip="加载网卡数据中..." />
</div>
);
}
const totalStats = networkCards.reduce(
(acc, card) => {
const stats = card.stats || {};
acc.total += stats.total || 0;
acc.free += stats.free || 0;
acc.occupied += stats.occupied || 0;
acc.fault += stats.fault || 0;
return acc;
},
{ total: 0, free: 0, occupied: 0, fault: 0 }
);
if (networkCards.length === 0) {
return <Empty description="该设备暂无网卡和端口" />;
}
return (
<div className="network-card-panel">
<div
style={{
display: 'flex',
gap: '24px',
marginBottom: '16px',
padding: '12px 16px',
background: '#f8fafc',
borderRadius: '8px',
}}
>
<Space size={16}>
<Badge
count={networkCards.filter(c => !c.isUngrouped).length}
style={{ backgroundColor: designTokens.colors.primary.main }}
/>
<span style={{ color: '#64748b', fontSize: '13px' }}>个网卡</span>
<Badge count={totalStats.total} style={{ backgroundColor: '#667eea' }} />
<span style={{ color: '#64748b', fontSize: '13px' }}>个端口</span>
</Space>
</div>
<Collapse
activeKey={expandedCards.filter(id =>
paginatedNetworkCards.some(card => card.nicId === id)
)}
onChange={keys => setExpandedCards(keys)}
expandIconPosition="end"
style={{ background: 'transparent' }}
>
{paginatedNetworkCards.map(card => (
<Panel
key={card.nicId}
header={renderCardHeader(card)}
style={{
background: '#fff',
borderRadius: '8px',
marginBottom: '8px',
border: '1px solid #e2e8f0',
}}
>
{card.ports && card.ports.length > 0 ? (
renderPortTable(card.ports)
) : (
<div style={{ padding: '24px', textAlign: 'center', color: '#94a3b8' }}>
{card.isUngrouped ? '分组' : '网卡'}暂无端口
</div>
)}
</Panel>
))}
</Collapse>
{portsTotalPages > 1 && (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginTop: 16,
gap: 12,
}}
>
<Button
icon={<LeftOutlined />}
disabled={portsPage === 1}
onClick={() => setPortsPage(prev => prev - 1)}
size="small"
/>
<span style={{ color: '#64748b', fontSize: '13px' }}>
{portsPage} / {portsTotalPages}
</span>
<Button
icon={<RightOutlined />}
disabled={portsPage === portsTotalPages}
onClick={() => setPortsPage(prev => prev + 1)}
size="small"
/>
</div>
)}
</div>
);
};
const renderCablesTab = () => {
if (deviceCables.length === 0) {
return <Empty description="该设备暂无接线" />;
}
return (
<div className="cable-panel">
<Space direction="vertical" size={16} style={{ width: '100%' }}>
{paginatedCables.map(cable => (
<Card
key={cable.cableId}
size="small"
style={{ borderRadius: '8px' }}
>
<Row gutter={[16, 8]}>
<Col span={12}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: 4 }}>源设备</div>
<div style={{ fontWeight: 500 }}>
{cable.sourceDevice?.name || '-'}
<Tag color="blue" style={{ marginLeft: '8px' }}>
{cable.sourcePort}
</Tag>
</div>
</Col>
<Col span={12}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: 4 }}>目标设备</div>
<div style={{ fontWeight: 500 }}>
{cable.targetDevice?.name || '-'}
<Tag color="green" style={{ marginLeft: '8px' }}>
{cable.targetPort}
</Tag>
</div>
</Col>
<Col span={24}>
<Space wrap style={{ marginTop: 8 }}>
<Tag
color={
cable.status === 'normal'
? 'success'
: cable.status === 'fault'
? 'error'
: 'default'
}
>
{cable.status === 'normal'
? '正常'
: cable.status === 'fault'
? '故障'
: '未连接'}
</Tag>
<Tag color="purple">
{cable.cableType === 'ethernet'
? '网线'
: cable.cableType === 'fiber'
? '光纤'
: '铜缆'}
</Tag>
{cable.cableLength && <Tag color="orange">{cable.cableLength}m</Tag>}
</Space>
</Col>
{cable.description && (
<Col span={24}>
<div style={{ fontSize: '12px', color: '#666', marginTop: 4 }}>
{cable.description}
</div>
</Col>
)}
</Row>
</Card>
))}
</Space>
{cablesTotalPages > 1 && (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginTop: 16,
gap: 12,
}}
>
<Button
icon={<LeftOutlined />}
disabled={cablesPage === 1}
onClick={() => setCablesPage(prev => prev - 1)}
size="small"
/>
<span style={{ color: '#64748b', fontSize: '13px' }}>
{cablesPage} / {cablesTotalPages}
</span>
<Button
icon={<RightOutlined />}
disabled={cablesPage === cablesTotalPages}
onClick={() => setCablesPage(prev => prev + 1)}
size="small"
/>
</div>
)}
</div>
);
};
if (!device) return null;
// 解析自定义字段
const customFields = device.customFields || {};
const standardFields = ['deviceId', 'name', 'type', 'model', 'serialNumber', 'status', 'ipAddress', 'position', 'height', 'powerConsumption', 'purchaseDate', 'warrantyExpiry', 'description'];
const customFieldEntries = Object.entries(customFields).filter(([key]) => !standardFields.includes(key));
@@ -213,17 +572,10 @@ function DeviceDetailDrawer({
label: (
<span>
<ApiOutlined />
端口与网卡
端口与网卡 ({networkCards.length})
</span>
),
children: (
<NetworkCardPanel
deviceId={device?.deviceId}
deviceName={device?.name}
onRefresh={onRefreshCables}
refreshTrigger={refreshTrigger}
/>
),
children: renderPortsTab(),
},
{
key: 'cables',
@@ -233,88 +585,7 @@ function DeviceDetailDrawer({
接线 ({deviceCables.length})
</span>
),
children: (
<div className="cable-panel">
{deviceCables.length === 0 ? (
<Empty description="该设备暂无接线" />
) : (
<Space direction="vertical" size={16} style={{ width: '100%' }}>
{deviceCables.map(cable => (
<Card
key={cable.cableId}
size="small"
style={{ borderRadius: '8px' }}
extra={
<Popconfirm
title="确定要删除这条接线吗?"
onConfirm={() => onDeleteCable?.(cable.cableId)}
okText="确定"
cancelText="取消"
>
<Button type="text" danger icon={<DeleteOutlined />} size="small" />
</Popconfirm>
}
>
<Row gutter={[16, 8]}>
<Col span={12}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: 4 }}>源设备</div>
<div style={{ fontWeight: 500 }}>
{cable.sourceDevice?.name || '-'}
<Tag color="blue" style={{ marginLeft: '8px' }}>
{cable.sourcePort}
</Tag>
</div>
</Col>
<Col span={12}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: 4 }}>目标设备</div>
<div style={{ fontWeight: 500 }}>
{cable.targetDevice?.name || '-'}
<Tag color="green" style={{ marginLeft: '8px' }}>
{cable.targetPort}
</Tag>
</div>
</Col>
<Col span={24}>
<Space wrap style={{ marginTop: 8 }}>
<Tag
color={
cable.status === 'normal'
? 'success'
: cable.status === 'fault'
? 'error'
: 'default'
}
>
{cable.status === 'normal'
? '正常'
: cable.status === 'fault'
? '故障'
: '未连接'}
</Tag>
<Tag color="purple">
{cable.cableType === 'ethernet'
? '网线'
: cable.cableType === 'fiber'
? '光纤'
: '铜缆'}
</Tag>
{cable.cableLength && <Tag color="orange">{cable.cableLength}m</Tag>}
</Space>
</Col>
{cable.description && (
<Col span={24}>
<div style={{ fontSize: '12px', color: '#666', marginTop: 4 }}>
{cable.description}
</div>
</Col>
)}
</Row>
</Card>
))}
</Space>
)}
</div>
),
children: renderCablesTab(),
},
{
key: 'tickets',
@@ -326,25 +597,21 @@ function DeviceDetailDrawer({
),
children: (
<div className="tickets-panel">
<div style={{ marginBottom: 16 }}>
<Button
type="primary"
icon={<ToolOutlined />}
onClick={() => onCreateTicket?.(device)}
>
创建工单
</Button>
</div>
<Table
columns={ticketColumns}
dataSource={tickets}
rowKey="ticketId"
loading={ticketsLoading}
pagination={{
...ticketsPagination,
current: ticketsPagination.current,
pageSize: ticketsPagination.pageSize,
total: ticketsPagination.total,
onChange: (page, pageSize) => fetchDeviceTickets(page, pageSize),
showSizeChanger: false,
size: 'small',
}}
size="small"
locale={{ emptyText: <Empty description="该设备暂无工单记录" /> }}
/>
</div>
),
@@ -364,32 +631,14 @@ function DeviceDetailDrawer({
open={visible}
onClose={onClose}
extra={
<Space>
<Tooltip title="编辑设备信息">
<Button icon={<EditOutlined />} onClick={() => onEdit?.(device)}>
编辑
</Button>
</Tooltip>
<Tooltip title="添加网卡">
<Button icon={<PlusCircleOutlined />} onClick={() => onAddNic?.(device)}>
加网卡
</Button>
</Tooltip>
<Tooltip title="添加端口">
<Button icon={<ApiOutlined />} onClick={() => onAddPort?.(device)}>
加端口
</Button>
</Tooltip>
<Tooltip title="添加接线">
<Button icon={<EnvironmentOutlined />} onClick={() => onAddCable?.(device)}>
加接线
</Button>
</Tooltip>
</Space>
<Tooltip title="编辑设备信息">
<Button icon={<EditOutlined />} onClick={() => onEdit?.(device)}>
编辑
</Button>
</Tooltip>
}
styles={{ body: { padding: '0', overflow: 'auto' } }}
>
{/* 设备头部信息 */}
<div style={{ padding: '20px 24px', background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', color: '#fff' }}>
<Row gutter={[16, 16]} align="middle">
<Col>
@@ -408,9 +657,7 @@ function DeviceDetailDrawer({
</Row>
</div>
{/* 设备信息内容 */}
<div style={{ padding: '20px 24px' }}>
{/* 基本信息卡片 */}
<Card title="基本信息" size="small" style={{ marginBottom: 16 }}>
<Row gutter={[24, 16]}>
<Col span={8}>
@@ -440,7 +687,6 @@ function DeviceDetailDrawer({
</Row>
</Card>
{/* 维保信息卡片 */}
<Card title="维保信息" size="small" style={{ marginBottom: 16 }}>
<Row gutter={[24, 16]}>
<Col span={12}>
@@ -458,14 +704,12 @@ function DeviceDetailDrawer({
</Row>
</Card>
{/* 描述信息 */}
{device.description && (
<Card title="描述" size="small" style={{ marginBottom: 16 }}>
<div style={{ whiteSpace: 'pre-wrap', lineHeight: 1.6 }}>{device.description}</div>
</Card>
)}
{/* 自定义字段卡片 */}
{customFieldEntries.length > 0 && (
<Card title="自定义字段" size="small" style={{ marginBottom: 16 }}>
<Row gutter={[24, 16]}>
@@ -484,7 +728,6 @@ function DeviceDetailDrawer({
<Divider style={{ margin: '24px 0' }} />
{/* 标签页 */}
<Tabs activeKey={activeTab} onChange={setActiveTab} items={tabItems} />
</div>
</Drawer>
@@ -1,9 +1,10 @@
import React, { useState, useEffect } from 'react';
import { Modal, Form, Input, Select, InputNumber, DatePicker, Switch, Row, Col, Button, Space } from 'antd';
import { PlusOutlined, EditOutlined, DatabaseOutlined } from '@ant-design/icons';
import { Modal, Form, Input, Select, InputNumber, DatePicker, Switch, Row, Col, Button, Space, Alert } from 'antd';
import { PlusOutlined, EditOutlined, DatabaseOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { designTokens } from '../../config/theme';
import { getFormInitialValues, prepareDeviceFormData } from '../../utils/deviceUtils.jsx';
import { deviceAPI } from '../../api';
const { Option } = Select;
@@ -31,6 +32,9 @@ const DeviceFormModal = ({
}) => {
const [form] = Form.useForm();
const [selectedRoomId, setSelectedRoomId] = useState(null);
const [selectedRackId, setSelectedRackId] = useState(null);
const [positionConflict, setPositionConflict] = useState(null);
const [checkingPosition, setCheckingPosition] = useState(false);
useEffect(() => {
if (visible) {
@@ -47,22 +51,91 @@ const DeviceFormModal = ({
const rack = racks.find((r) => r.rackId === editingDevice.rackId);
if (rack) {
setSelectedRoomId(rack.roomId);
setSelectedRackId(editingDevice.rackId);
}
}
if (editingDevice.position) {
checkPositionConflict(editingDevice.rackId, editingDevice.position, editingDevice.height, editingDevice.deviceId);
}
} else {
form.resetFields();
setSelectedRoomId(null);
setSelectedRackId(null);
setPositionConflict(null);
}
}
}, [visible, editingDevice, racks, form]);
const checkPositionConflict = async (rackId, position, height, deviceId = null) => {
if (!rackId || !position) {
setPositionConflict(null);
return;
}
setCheckingPosition(true);
try {
const params = {
position,
height: height || 1,
};
if (deviceId) {
params.excludeDeviceId = deviceId;
}
const result = await deviceAPI.checkPosition(rackId, params);
if (!result.available) {
setPositionConflict(result.reason);
} else {
setPositionConflict(null);
}
} catch (error) {
console.error('检查U位冲突失败:', error);
setPositionConflict(null);
} finally {
setCheckingPosition(false);
}
};
const handleRackChange = (value) => {
setSelectedRackId(value);
const position = form.getFieldValue('position');
const height = form.getFieldValue('height');
if (position) {
checkPositionConflict(value, position, height, editingDevice?.deviceId);
} else {
setPositionConflict(null);
}
};
const handlePositionChange = (value) => {
const height = form.getFieldValue('height');
if (selectedRackId && value) {
checkPositionConflict(selectedRackId, value, height, editingDevice?.deviceId);
} else {
setPositionConflict(null);
}
};
const handleHeightChange = (value) => {
const position = form.getFieldValue('position');
if (selectedRackId && position) {
checkPositionConflict(selectedRackId, position, value, editingDevice?.deviceId);
} else {
setPositionConflict(null);
}
};
const handleSubmit = (values) => {
if (positionConflict) {
return;
}
const deviceData = prepareDeviceFormData(values, !!editingDevice);
onSubmit(deviceData);
};
const handleRoomChange = (value) => {
setSelectedRoomId(value);
setSelectedRackId(null);
setPositionConflict(null);
form.setFieldValue('rackId', undefined);
};
@@ -123,7 +196,7 @@ const DeviceFormModal = ({
};
const filteredFields = deviceFields.filter(
(field) => field.fieldName !== 'deviceId' && field.fieldName !== 'rackId'
(field) => field.fieldName !== 'deviceId' && field.fieldName !== 'rackId' && field.fieldName !== 'position' && field.fieldName !== 'height'
);
const formItems = [];
@@ -219,6 +292,7 @@ const DeviceFormModal = ({
disabled={!selectedRoomId}
showSearch
optionFilterProp="children"
onChange={handleRackChange}
>
{(selectedRoomId ? racks.filter((rack) => rack.roomId === selectedRoomId) : []).map(
(rack) => (
@@ -231,6 +305,61 @@ const DeviceFormModal = ({
</Form.Item>
</Col>
</Row>
<Row gutter={16} style={{ marginTop: '16px' }}>
<Col span={12}>
<Form.Item
name="position"
label={
<span>
安装位置 (U位)
<span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>
</span>
}
rules={[{ required: true, message: '请输入U位' }]}
style={{ marginBottom: '0' }}
>
<InputNumber
placeholder="如: 1"
min={1}
max={42}
style={{ width: '100%', borderRadius: '8px' }}
onChange={handlePositionChange}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="height"
label={
<span>
设备高度 (U)
<span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>
</span>
}
rules={[{ required: true, message: '请输入设备高度' }]}
initialValue={1}
style={{ marginBottom: '0' }}
>
<InputNumber
placeholder="如: 2"
min={1}
max={10}
style={{ width: '100%', borderRadius: '8px' }}
onChange={handleHeightChange}
/>
</Form.Item>
</Col>
</Row>
{positionConflict && (
<div style={{ marginTop: '12px' }}>
<Alert
message={positionConflict}
type="error"
showIcon
icon={<ExclamationCircleOutlined />}
/>
</div>
)}
</div>
</Col>
</React.Fragment>