refactor: 统一代码风格并迁移至 ESLint 新配置
style(backend): 格式化模型文件代码 style(frontend): 调整组件代码格式 chore: 删除旧 ESLint 配置并添加新配置 refactor(backend): 重构模型定义语法 style: 统一箭头函数和对象属性简写
This commit is contained in:
@@ -47,7 +47,7 @@ const CascadingRackPanel = ({
|
||||
const lowerSearch = searchText.toLowerCase().trim();
|
||||
|
||||
return rooms
|
||||
.map((room) => {
|
||||
.map(room => {
|
||||
const roomNameMatch = room.name?.toLowerCase().includes(lowerSearch);
|
||||
const roomIdMatch = room.roomId?.toLowerCase().includes(lowerSearch);
|
||||
|
||||
@@ -56,7 +56,7 @@ const CascadingRackPanel = ({
|
||||
}
|
||||
|
||||
const filteredRacks = room.racks.filter(
|
||||
(rack) =>
|
||||
rack =>
|
||||
rack.name?.toLowerCase().includes(lowerSearch) ||
|
||||
rack.rackId?.toLowerCase().includes(lowerSearch)
|
||||
);
|
||||
@@ -67,13 +67,13 @@ const CascadingRackPanel = ({
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((room) => room !== null);
|
||||
.filter(room => room !== null);
|
||||
}, [rooms, searchText]);
|
||||
|
||||
const flatRackList = useMemo(() => {
|
||||
const list = [];
|
||||
filteredRooms.forEach((room) => {
|
||||
room.racks.forEach((rack) => {
|
||||
filteredRooms.forEach(room => {
|
||||
room.racks.forEach(rack => {
|
||||
list.push({ ...rack, roomKey: room.key, roomName: room.name });
|
||||
});
|
||||
});
|
||||
@@ -81,7 +81,7 @@ const CascadingRackPanel = ({
|
||||
}, [filteredRooms]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
const handleKeyDown = e => {
|
||||
if (!visible) return;
|
||||
|
||||
switch (e.key) {
|
||||
@@ -91,11 +91,11 @@ const CascadingRackPanel = ({
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
setFocusedIndex((prev) => Math.min(prev + 1, flatRackList.length - 1));
|
||||
setFocusedIndex(prev => Math.min(prev + 1, flatRackList.length - 1));
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
setFocusedIndex((prev) => Math.max(prev - 1, 0));
|
||||
setFocusedIndex(prev => Math.max(prev - 1, 0));
|
||||
break;
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
@@ -113,13 +113,13 @@ const CascadingRackPanel = ({
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [visible, focusedIndex, flatRackList, onSelect, onClose]);
|
||||
|
||||
const getUsageColor = (percent) => {
|
||||
const getUsageColor = percent => {
|
||||
if (percent >= 90) return '#ef4444';
|
||||
if (percent >= 70) return '#f59e0b';
|
||||
return '#22c55e';
|
||||
};
|
||||
|
||||
const getUsageBadgeStatus = (percent) => {
|
||||
const getUsageBadgeStatus = percent => {
|
||||
if (percent >= 90) return 'error';
|
||||
if (percent >= 70) return 'warning';
|
||||
return 'success';
|
||||
@@ -132,7 +132,7 @@ const CascadingRackPanel = ({
|
||||
[onSelect]
|
||||
);
|
||||
|
||||
const handlePanelClick = useCallback((e) => {
|
||||
const handlePanelClick = useCallback(e => {
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
@@ -191,7 +191,7 @@ const CascadingRackPanel = ({
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
value={searchText}
|
||||
onChange={(e) => {
|
||||
onChange={e => {
|
||||
setSearchText(e.target.value);
|
||||
setFocusedIndex(-1);
|
||||
}}
|
||||
@@ -243,13 +243,9 @@ const CascadingRackPanel = ({
|
||||
gap: 8,
|
||||
cursor: 'pointer',
|
||||
background:
|
||||
activeRoomKey === room.key
|
||||
? 'rgba(59, 130, 246, 0.1)'
|
||||
: 'transparent',
|
||||
activeRoomKey === room.key ? 'rgba(59, 130, 246, 0.1)' : 'transparent',
|
||||
}}
|
||||
onClick={() =>
|
||||
setActiveRoomKey(activeRoomKey === room.key ? null : room.key)
|
||||
}
|
||||
onClick={() => setActiveRoomKey(activeRoomKey === room.key ? null : room.key)}
|
||||
>
|
||||
<DatabaseOutlined style={{ color: '#60a5fa', fontSize: 14 }} />
|
||||
<span
|
||||
@@ -285,9 +281,7 @@ const CascadingRackPanel = ({
|
||||
{activeRoomKey === room.key && (
|
||||
<div style={{ paddingLeft: 16 }}>
|
||||
{room.racks.map((rack, rackIndex) => {
|
||||
const globalIndex = flatRackList.findIndex(
|
||||
(r) => r.rackId === rack.rackId
|
||||
);
|
||||
const globalIndex = flatRackList.findIndex(r => r.rackId === rack.rackId);
|
||||
const deviceCount = rack.Devices?.length || rack.deviceCount || 0;
|
||||
const height = rack.height || 45;
|
||||
const usedU = deviceCount * 2;
|
||||
@@ -313,9 +307,7 @@ const CascadingRackPanel = ({
|
||||
margin: '2px 8px',
|
||||
transition: 'all 0.15s ease',
|
||||
borderLeft: isSelected ? '3px solid #3b82f6' : '3px solid transparent',
|
||||
background: isHovered
|
||||
? 'rgba(59, 130, 246, 0.15)'
|
||||
: 'transparent',
|
||||
background: isHovered ? 'rgba(59, 130, 246, 0.15)' : 'transparent',
|
||||
}}
|
||||
onClick={() => handleRackSelect(rack, room)}
|
||||
onMouseEnter={() => setHoveredRackId(rack.rackId)}
|
||||
@@ -406,4 +398,4 @@ const CascadingRackPanel = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default CascadingRackPanel;
|
||||
export default CascadingRackPanel;
|
||||
|
||||
@@ -54,7 +54,7 @@ const RackSelectorHeader = ({
|
||||
const { screenSize, config, isMobile } = useResponsiveLayout();
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
const handleClickOutside = event => {
|
||||
if (selectorRef.current && !selectorRef.current.contains(event.target)) {
|
||||
setSelectorVisible(false);
|
||||
}
|
||||
@@ -77,40 +77,35 @@ const RackSelectorHeader = ({
|
||||
[onRackSelect]
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
setSelectorVisible(false);
|
||||
}
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
setSelectorVisible((prev) => !prev);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
const handleKeyDown = useCallback(e => {
|
||||
if (e.key === 'Escape') {
|
||||
setSelectorVisible(false);
|
||||
}
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
setSelectorVisible(prev => !prev);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [handleKeyDown]);
|
||||
|
||||
const selectedRoom = rooms.find((r) => r.key === selectedRoomKey);
|
||||
const selectedRoom = rooms.find(r => r.key === selectedRoomKey);
|
||||
const displayText = selectedRack
|
||||
? `${selectedRoom?.name || ''} / ${selectedRack.name}`
|
||||
: '选择机房 / 机柜';
|
||||
|
||||
const canNavigatePrev =
|
||||
racksInSelectedRoom && racksInSelectedRoom.length > 1 && selectedRack;
|
||||
const canNavigateNext =
|
||||
racksInSelectedRoom && racksInSelectedRoom.length > 1 && selectedRack;
|
||||
const canNavigatePrev = racksInSelectedRoom && racksInSelectedRoom.length > 1 && selectedRack;
|
||||
const canNavigateNext = racksInSelectedRoom && racksInSelectedRoom.length > 1 && selectedRack;
|
||||
|
||||
const getCurrentRackIndex = () => {
|
||||
if (!selectedRack || !racksInSelectedRoom) return -1;
|
||||
return racksInSelectedRoom.findIndex((r) => r.rackId === selectedRack.rackId);
|
||||
return racksInSelectedRoom.findIndex(r => r.rackId === selectedRack.rackId);
|
||||
};
|
||||
|
||||
const dropdownMenuItems = ACTION_BUTTONS_CONFIG.map((btn) => ({
|
||||
const dropdownMenuItems = ACTION_BUTTONS_CONFIG.map(btn => ({
|
||||
key: btn.key,
|
||||
label: (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
@@ -190,13 +185,11 @@ const RackSelectorHeader = ({
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={() => setSelectorVisible((prev) => !prev)}
|
||||
onClick={() => setSelectorVisible(prev => !prev)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
background: selectorVisible
|
||||
? 'rgba(59, 130, 246, 0.15)'
|
||||
: 'rgba(255, 255, 255, 0.08)',
|
||||
background: selectorVisible ? 'rgba(59, 130, 246, 0.15)' : 'rgba(255, 255, 255, 0.08)',
|
||||
border: selectorVisible
|
||||
? '1px solid rgba(59, 130, 246, 0.5)'
|
||||
: '1px solid rgba(255, 255, 255, 0.12)',
|
||||
@@ -247,7 +240,7 @@ const RackSelectorHeader = ({
|
||||
>
|
||||
{canNavigatePrev && (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onPrevRack();
|
||||
}}
|
||||
@@ -262,10 +255,10 @@ const RackSelectorHeader = ({
|
||||
fontSize: 12,
|
||||
transition: 'all 0.15s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.background = 'rgba(255,255,255,0.15)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
|
||||
}}
|
||||
>
|
||||
@@ -274,7 +267,7 @@ const RackSelectorHeader = ({
|
||||
)}
|
||||
{canNavigateNext && (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onNextRack();
|
||||
}}
|
||||
@@ -289,10 +282,10 @@ const RackSelectorHeader = ({
|
||||
fontSize: 12,
|
||||
transition: 'all 0.15s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.background = 'rgba(255,255,255,0.15)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
|
||||
}}
|
||||
>
|
||||
@@ -420,11 +413,7 @@ const RackSelectorHeader = ({
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0 }}>
|
||||
{renderDeviceSlideToggle()}
|
||||
<Dropdown
|
||||
menu={{ items: dropdownMenuItems }}
|
||||
trigger={['click']}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Dropdown menu={{ items: dropdownMenuItems }} trigger={['click']} placement="bottomRight">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<MenuOutlined style={{ color: 'rgba(255,255,255,0.8)' }} />}
|
||||
@@ -487,4 +476,4 @@ const RackSelectorHeader = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default RackSelectorHeader;
|
||||
export default RackSelectorHeader;
|
||||
|
||||
@@ -66,7 +66,9 @@ function BatchImportModal({ visible, onClose, onImportNetworkCard, onImportPort
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '16px', fontWeight: 600 }}>批量导入网卡</div>
|
||||
<div style={{ fontSize: '12px', color: designTokens.colors.neutral[500], fontWeight: 400 }}>
|
||||
<div
|
||||
style={{ fontSize: '12px', color: designTokens.colors.neutral[500], fontWeight: 400 }}
|
||||
>
|
||||
用于服务器设备
|
||||
</div>
|
||||
</Button>
|
||||
@@ -90,7 +92,9 @@ function BatchImportModal({ visible, onClose, onImportNetworkCard, onImportPort
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '16px', fontWeight: 600 }}>批量导入端口</div>
|
||||
<div style={{ fontSize: '12px', color: designTokens.colors.neutral[500], fontWeight: 400 }}>
|
||||
<div
|
||||
style={{ fontSize: '12px', color: designTokens.colors.neutral[500], fontWeight: 400 }}
|
||||
>
|
||||
用于所有设备
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
@@ -55,7 +55,7 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
|
||||
setTargetPorts([]);
|
||||
setDevices([]);
|
||||
devicesRef.current = [];
|
||||
|
||||
|
||||
fetchDevices().then(deviceList => {
|
||||
console.log('[CableCreateModal] Devices fetched:', deviceList.length);
|
||||
const sourceDeviceId = sourceDevice?.deviceId || sourceDevice?.id;
|
||||
@@ -157,7 +157,11 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
|
||||
maskClosable={false}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item label="源设备" name="sourceDeviceId" rules={[{ required: true, message: '请选择源设备' }]}>
|
||||
<Form.Item
|
||||
label="源设备"
|
||||
name="sourceDeviceId"
|
||||
rules={[{ required: true, message: '请选择源设备' }]}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
filterOption={false}
|
||||
@@ -175,11 +179,12 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="源端口" name="sourcePort" rules={[{ required: true, message: '请选择源端口' }]}>
|
||||
<Select
|
||||
placeholder="请先选择设备"
|
||||
disabled={!sourcePorts.length}
|
||||
>
|
||||
<Form.Item
|
||||
label="源端口"
|
||||
name="sourcePort"
|
||||
rules={[{ required: true, message: '请选择源端口' }]}
|
||||
>
|
||||
<Select placeholder="请先选择设备" disabled={!sourcePorts.length}>
|
||||
{sourcePorts.map(port => (
|
||||
<Option key={port.portId} value={port.portName}>
|
||||
{port.portName}
|
||||
@@ -188,7 +193,11 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="目标设备" name="targetDeviceId" rules={[{ required: true, message: '请选择目标设备' }]}>
|
||||
<Form.Item
|
||||
label="目标设备"
|
||||
name="targetDeviceId"
|
||||
rules={[{ required: true, message: '请选择目标设备' }]}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
filterOption={false}
|
||||
@@ -206,11 +215,12 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="目标端口" name="targetPort" rules={[{ required: true, message: '请选择目标端口' }]}>
|
||||
<Select
|
||||
placeholder="请先选择设备"
|
||||
disabled={!targetPorts.length}
|
||||
>
|
||||
<Form.Item
|
||||
label="目标端口"
|
||||
name="targetPort"
|
||||
rules={[{ required: true, message: '请选择目标端口' }]}
|
||||
>
|
||||
<Select placeholder="请先选择设备" disabled={!targetPorts.length}>
|
||||
{targetPorts.map(port => (
|
||||
<Option key={port.portId} value={port.portName}>
|
||||
{port.portName}
|
||||
|
||||
@@ -135,10 +135,17 @@ export const DangerConfirmModal = ({
|
||||
}
|
||||
description={
|
||||
<Paragraph style={{ marginBottom: 0, marginTop: 8 }}>
|
||||
{description || `确定要${operationLabel} ${itemCount > 1 ? `${itemCount} 个` : '该'}${entityLabel}吗?`}
|
||||
{description ||
|
||||
`确定要${operationLabel} ${itemCount > 1 ? `${itemCount} 个` : '该'}${entityLabel}吗?`}
|
||||
</Paragraph>
|
||||
}
|
||||
type={riskLevel === RISK_LEVEL.EXTREME ? 'error' : riskLevel === RISK_LEVEL.HIGH ? 'warning' : 'info'}
|
||||
type={
|
||||
riskLevel === RISK_LEVEL.EXTREME
|
||||
? 'error'
|
||||
: riskLevel === RISK_LEVEL.HIGH
|
||||
? 'warning'
|
||||
: 'info'
|
||||
}
|
||||
style={{
|
||||
backgroundColor: config.bgColor,
|
||||
borderColor: config.borderColor,
|
||||
@@ -177,7 +184,7 @@ export const DangerConfirmModal = ({
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
onChange={e => setKeyword(e.target.value)}
|
||||
placeholder={`请输入 ${config.keyword}`}
|
||||
status={keyword && !isKeywordValid ? 'error' : undefined}
|
||||
onPressEnter={handleOk}
|
||||
@@ -202,9 +209,7 @@ export const DangerConfirmModal = ({
|
||||
{typeof item === 'string' ? item : item.name || item.label || item}
|
||||
</Text>
|
||||
))}
|
||||
{itemCount > 5 && (
|
||||
<Text type="secondary">...还有 {itemCount - 5} 项</Text>
|
||||
)}
|
||||
{itemCount > 5 && <Text type="secondary">...还有 {itemCount - 5} 项</Text>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -217,9 +222,7 @@ export const DangerConfirmModal = ({
|
||||
title={
|
||||
<Space>
|
||||
<WarningOutlined style={{ color: config.color }} />
|
||||
<span style={{ color: config.color }}>
|
||||
{title || `${operationLabel}确认`}
|
||||
</span>
|
||||
<span style={{ color: config.color }}>{title || `${operationLabel}确认`}</span>
|
||||
</Space>
|
||||
}
|
||||
open={open}
|
||||
|
||||
@@ -49,7 +49,7 @@ function DeviceDetailDrawer({
|
||||
refreshTrigger,
|
||||
}) {
|
||||
const [activeTab, setActiveTab] = useState('ports');
|
||||
|
||||
|
||||
const [tickets, setTickets] = useState([]);
|
||||
const [ticketsLoading, setTicketsLoading] = useState(false);
|
||||
const [ticketsPagination, setTicketsPagination] = useState({
|
||||
@@ -91,26 +91,29 @@ function DeviceDetailDrawer({
|
||||
}
|
||||
}, [visible, device?.deviceId, fetchNetworkCards, refreshTrigger]);
|
||||
|
||||
const fetchDeviceTickets = useCallback(async (page = 1, pageSize = PAGE_SIZE) => {
|
||||
if (!device?.deviceId) return;
|
||||
setTicketsLoading(true);
|
||||
try {
|
||||
const response = await deviceAPI.getTickets(device.deviceId, {
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
setTickets(response.data || []);
|
||||
setTicketsPagination({
|
||||
current: response.page || 1,
|
||||
pageSize: response.pageSize || PAGE_SIZE,
|
||||
total: response.total || 0,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取设备工单失败:', error);
|
||||
} finally {
|
||||
setTicketsLoading(false);
|
||||
}
|
||||
}, [device?.deviceId]);
|
||||
const fetchDeviceTickets = useCallback(
|
||||
async (page = 1, pageSize = PAGE_SIZE) => {
|
||||
if (!device?.deviceId) return;
|
||||
setTicketsLoading(true);
|
||||
try {
|
||||
const response = await deviceAPI.getTickets(device.deviceId, {
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
setTickets(response.data || []);
|
||||
setTicketsPagination({
|
||||
current: response.page || 1,
|
||||
pageSize: response.pageSize || PAGE_SIZE,
|
||||
total: response.total || 0,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取设备工单失败:', error);
|
||||
} finally {
|
||||
setTicketsLoading(false);
|
||||
}
|
||||
},
|
||||
[device?.deviceId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && device?.deviceId && activeTab === 'tickets') {
|
||||
@@ -118,63 +121,66 @@ function DeviceDetailDrawer({
|
||||
}
|
||||
}, [visible, device?.deviceId, activeTab, fetchDeviceTickets]);
|
||||
|
||||
const ticketColumns = useMemo(() => [
|
||||
{
|
||||
title: '工单编号',
|
||||
dataIndex: 'ticketId',
|
||||
key: 'ticketId',
|
||||
width: 120,
|
||||
render: (text) => <Text code>{text}</Text>,
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 90,
|
||||
render: (status) => {
|
||||
const statusConfig = {
|
||||
pending: { color: 'warning', text: '待处理' },
|
||||
processing: { color: 'processing', text: '处理中' },
|
||||
completed: { color: 'success', text: '已完成' },
|
||||
closed: { color: 'default', text: '已关闭' },
|
||||
};
|
||||
const config = statusConfig[status] || { color: 'default', text: status };
|
||||
return <Badge status={config.color} text={config.text} />;
|
||||
const ticketColumns = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: '工单编号',
|
||||
dataIndex: 'ticketId',
|
||||
key: 'ticketId',
|
||||
width: 120,
|
||||
render: text => <Text code>{text}</Text>,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '优先级',
|
||||
dataIndex: 'priority',
|
||||
key: 'priority',
|
||||
width: 80,
|
||||
render: (priority) => {
|
||||
const priorityConfig = {
|
||||
low: { color: 'success', text: '低' },
|
||||
medium: { color: 'warning', text: '中' },
|
||||
high: { color: 'error', text: '高' },
|
||||
critical: { color: 'purple', text: '紧急' },
|
||||
};
|
||||
const config = priorityConfig[priority] || { color: 'default', text: priority };
|
||||
return <Tag color={config.color}>{config.text}</Tag>;
|
||||
{
|
||||
title: '标题',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
ellipsis: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 150,
|
||||
render: (date) => {
|
||||
if (!date) return '-';
|
||||
return dayjs(date).format('YYYY-MM-DD HH:mm');
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 90,
|
||||
render: status => {
|
||||
const statusConfig = {
|
||||
pending: { color: 'warning', text: '待处理' },
|
||||
processing: { color: 'processing', text: '处理中' },
|
||||
completed: { color: 'success', text: '已完成' },
|
||||
closed: { color: 'default', text: '已关闭' },
|
||||
};
|
||||
const config = statusConfig[status] || { color: 'default', text: status };
|
||||
return <Badge status={config.color} text={config.text} />;
|
||||
},
|
||||
},
|
||||
},
|
||||
], []);
|
||||
{
|
||||
title: '优先级',
|
||||
dataIndex: 'priority',
|
||||
key: 'priority',
|
||||
width: 80,
|
||||
render: priority => {
|
||||
const priorityConfig = {
|
||||
low: { color: 'success', text: '低' },
|
||||
medium: { color: 'warning', text: '中' },
|
||||
high: { color: 'error', text: '高' },
|
||||
critical: { color: 'purple', text: '紧急' },
|
||||
};
|
||||
const config = priorityConfig[priority] || { color: 'default', text: priority };
|
||||
return <Tag color={config.color}>{config.text}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 150,
|
||||
render: date => {
|
||||
if (!date) return '-';
|
||||
return dayjs(date).format('YYYY-MM-DD HH:mm');
|
||||
},
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const deviceCables = useMemo(() => {
|
||||
if (!device || !cables) return [];
|
||||
@@ -237,7 +243,7 @@ function DeviceDetailDrawer({
|
||||
return typeMap[type?.toLowerCase()] || type || '未知设备';
|
||||
}, []);
|
||||
|
||||
const renderPortTable = (ports) => {
|
||||
const renderPortTable = ports => {
|
||||
const columns = [
|
||||
{
|
||||
title: '端口名称',
|
||||
@@ -397,7 +403,7 @@ function DeviceDetailDrawer({
|
||||
</div>
|
||||
|
||||
<Collapse
|
||||
activeKey={expandedCards.filter(id =>
|
||||
activeKey={expandedCards.filter(id =>
|
||||
paginatedNetworkCards.some(card => card.nicId === id)
|
||||
)}
|
||||
onChange={keys => setExpandedCards(keys)}
|
||||
@@ -466,11 +472,7 @@ function DeviceDetailDrawer({
|
||||
<div className="cable-panel">
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
{paginatedCables.map(cable => (
|
||||
<Card
|
||||
key={cable.cableId}
|
||||
size="small"
|
||||
style={{ borderRadius: '8px' }}
|
||||
>
|
||||
<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>
|
||||
@@ -563,8 +565,24 @@ function DeviceDetailDrawer({
|
||||
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));
|
||||
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)
|
||||
);
|
||||
|
||||
const tabItems = [
|
||||
{
|
||||
@@ -639,7 +657,13 @@ function DeviceDetailDrawer({
|
||||
}
|
||||
styles={{ body: { padding: '0', overflow: 'auto' } }}
|
||||
>
|
||||
<div style={{ padding: '20px 24px', background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', color: '#fff' }}>
|
||||
<div
|
||||
style={{
|
||||
padding: '20px 24px',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
<Row gutter={[16, 16]} align="middle">
|
||||
<Col>
|
||||
<DesktopOutlined style={{ fontSize: 48, opacity: 0.9 }} />
|
||||
@@ -682,7 +706,9 @@ function DeviceDetailDrawer({
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<div style={{ color: '#666', fontSize: '12px', marginBottom: 4 }}>功耗</div>
|
||||
<div style={{ fontWeight: 500 }}>{device.powerConsumption ? `${device.powerConsumption}W` : '-'}</div>
|
||||
<div style={{ fontWeight: 500 }}>
|
||||
{device.powerConsumption ? `${device.powerConsumption}W` : '-'}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
@@ -717,7 +743,9 @@ function DeviceDetailDrawer({
|
||||
const fieldLabel = tooltipFields?.[key]?.label || key;
|
||||
return (
|
||||
<Col span={8} key={key}>
|
||||
<div style={{ color: '#666', fontSize: '12px', marginBottom: 4 }}>{fieldLabel}</div>
|
||||
<div style={{ color: '#666', fontSize: '12px', marginBottom: 4 }}>
|
||||
{fieldLabel}
|
||||
</div>
|
||||
<div style={{ fontWeight: 500 }}>{String(value)}</div>
|
||||
</Col>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Button, Result, Space, Collapse, Typography } from 'antd';
|
||||
import {
|
||||
WarningOutlined,
|
||||
ReloadOutlined,
|
||||
HomeOutlined,
|
||||
BugOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { WarningOutlined, ReloadOutlined, HomeOutlined, BugOutlined } from '@ant-design/icons';
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
const { Panel } = Collapse;
|
||||
@@ -79,17 +74,10 @@ class ErrorBoundary extends Component {
|
||||
}
|
||||
extra={
|
||||
<Space size="middle">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={this.handleReload}
|
||||
>
|
||||
<Button type="primary" icon={<ReloadOutlined />} onClick={this.handleReload}>
|
||||
重新加载
|
||||
</Button>
|
||||
<Button
|
||||
icon={<HomeOutlined />}
|
||||
onClick={this.handleGoHome}
|
||||
>
|
||||
<Button icon={<HomeOutlined />} onClick={this.handleGoHome}>
|
||||
返回首页
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { Modal, Form, Input, Select, message, Space, Tooltip, Card, Alert, AutoComplete } from 'antd';
|
||||
import { CloudServerOutlined, InfoCircleOutlined, QuestionCircleOutlined, ThunderboltOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
message,
|
||||
Space,
|
||||
Tooltip,
|
||||
Card,
|
||||
Alert,
|
||||
AutoComplete,
|
||||
} from 'antd';
|
||||
import {
|
||||
CloudServerOutlined,
|
||||
InfoCircleOutlined,
|
||||
QuestionCircleOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import { designTokens } from '../config/theme';
|
||||
import CloseButton from './CloseButton';
|
||||
@@ -91,12 +107,14 @@ function NetworkCardCreateModal({ device, visible, onClose, onSuccess }) {
|
||||
width={800}
|
||||
styles={{ body: { padding: '0 24px 24px' } }}
|
||||
>
|
||||
<div style={{
|
||||
margin: '0 -24px 20px',
|
||||
padding: '16px 24px',
|
||||
background: `linear-gradient(135deg, ${designTokens.colors.primary.main}18 0%, ${designTokens.colors.primary.light}18 100%)`,
|
||||
borderBottom: `1px solid ${designTokens.colors.primary.light}30`,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
margin: '0 -24px 20px',
|
||||
padding: '16px 24px',
|
||||
background: `linear-gradient(135deg, ${designTokens.colors.primary.main}18 0%, ${designTokens.colors.primary.light}18 100%)`,
|
||||
borderBottom: `1px solid ${designTokens.colors.primary.light}30`,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '13px', color: designTokens.colors.neutral[700], lineHeight: 1.6 }}>
|
||||
为服务器添加新的网卡,网卡创建后可关联端口
|
||||
</div>
|
||||
@@ -107,9 +125,15 @@ function NetworkCardCreateModal({ device, visible, onClose, onSuccess }) {
|
||||
<div>
|
||||
<strong>插槽编号参考</strong>
|
||||
<ul style={{ margin: '8px 0 0', paddingLeft: '18px', lineHeight: 1.8 }}>
|
||||
<li><strong>LOM (LAN on Motherboard)</strong>:主板集成网卡,编号通常为 0</li>
|
||||
<li><strong>OCP (Open Compute Project)</strong>:服务器前端维护网卡专用槽位</li>
|
||||
<li><strong>PCIe 插槽</strong>:从 1 开始编号,对应服务器物理插槽位置</li>
|
||||
<li>
|
||||
<strong>LOM (LAN on Motherboard)</strong>:主板集成网卡,编号通常为 0
|
||||
</li>
|
||||
<li>
|
||||
<strong>OCP (Open Compute Project)</strong>:服务器前端维护网卡专用槽位
|
||||
</li>
|
||||
<li>
|
||||
<strong>PCIe 插槽</strong>:从 1 开始编号,对应服务器物理插槽位置
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
@@ -122,10 +146,7 @@ function NetworkCardCreateModal({ device, visible, onClose, onSuccess }) {
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
|
||||
<Card
|
||||
size="small"
|
||||
@@ -144,7 +165,11 @@ function NetworkCardCreateModal({ device, visible, onClose, onSuccess }) {
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={<span style={{ fontWeight: 500 }}>网卡名称 <span style={{ color: '#ff4d4f' }}>*</span></span>}
|
||||
label={
|
||||
<span style={{ fontWeight: 500 }}>
|
||||
网卡名称 <span style={{ color: '#ff4d4f' }}>*</span>
|
||||
</span>
|
||||
}
|
||||
rules={[
|
||||
{ required: true, message: '请输入网卡名称' },
|
||||
{ max: 50, message: '名称不能超过50个字符' },
|
||||
@@ -159,7 +184,9 @@ function NetworkCardCreateModal({ device, visible, onClose, onSuccess }) {
|
||||
<Space>
|
||||
<span style={{ fontWeight: 500 }}>插槽位置</span>
|
||||
<Tooltip title="参考上方提示选择或输入插槽位置">
|
||||
<InfoCircleOutlined style={{ color: designTokens.colors.neutral[400], cursor: 'help' }} />
|
||||
<InfoCircleOutlined
|
||||
style={{ color: designTokens.colors.neutral[400], cursor: 'help' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
@@ -181,7 +208,9 @@ function NetworkCardCreateModal({ device, visible, onClose, onSuccess }) {
|
||||
}))}
|
||||
filterOption={(input, option) =>
|
||||
option.value.toLowerCase().includes(input.toLowerCase()) ||
|
||||
option.label.props.children[0].props.children.toLowerCase().includes(input.toLowerCase())
|
||||
option.label.props.children[0].props.children
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase())
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -203,7 +232,10 @@ function NetworkCardCreateModal({ device, visible, onClose, onSuccess }) {
|
||||
styles={{ body: { padding: '16px 20px' } }}
|
||||
>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '12px' }}>
|
||||
<Form.Item name="manufacturer" label={<span style={{ fontWeight: 500 }}>制造商</span>}>
|
||||
<Form.Item
|
||||
name="manufacturer"
|
||||
label={<span style={{ fontWeight: 500 }}>制造商</span>}
|
||||
>
|
||||
<AutoComplete
|
||||
placeholder="选择或输入"
|
||||
options={MANUFACTURER_OPTIONS.map(m => ({ value: m.value, label: m.label }))}
|
||||
|
||||
@@ -334,26 +334,56 @@ function NetworkCardImportModal({ visible, onClose, onSuccess }) {
|
||||
message="操作说明"
|
||||
description={
|
||||
<div style={{ fontSize: '12px', lineHeight: '1.8' }}>
|
||||
<div><strong>适用范围:</strong>批量导入网卡仅适用于<span style={{ color: '#1890ff', fontWeight: 600 }}>服务器设备</span>,交换机设备请直接在端口管理中导入端口</div>
|
||||
<div style={{ marginTop: '8px' }}><strong>前置条件:</strong>请先在<span style={{ color: '#1890ff', fontWeight: 600 }}>设备管理</span>中添加目标服务器,确保设备ID已存在</div>
|
||||
<div style={{ marginTop: '8px' }}><strong>操作步骤:</strong></div>
|
||||
<div>
|
||||
<strong>适用范围:</strong>批量导入网卡仅适用于
|
||||
<span style={{ color: '#1890ff', fontWeight: 600 }}>服务器设备</span>
|
||||
,交换机设备请直接在端口管理中导入端口
|
||||
</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<strong>前置条件:</strong>请先在
|
||||
<span style={{ color: '#1890ff', fontWeight: 600 }}>设备管理</span>
|
||||
中添加目标服务器,确保设备ID已存在
|
||||
</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<strong>操作步骤:</strong>
|
||||
</div>
|
||||
<div style={{ paddingLeft: '12px', marginTop: '4px' }}>
|
||||
<div>1. 点击「下载模板」获取标准Excel/CSV文件</div>
|
||||
<div>2. 按模板格式填写网卡信息,<span style={{ color: '#ff4d4f', fontWeight: 600 }}>设备ID</span>和<span style={{ color: '#ff4d4f', fontWeight: 600 }}>网卡名称</span>为必填项</div>
|
||||
<div>
|
||||
2. 按模板格式填写网卡信息,
|
||||
<span style={{ color: '#ff4d4f', fontWeight: 600 }}>设备ID</span>和
|
||||
<span style={{ color: '#ff4d4f', fontWeight: 600 }}>网卡名称</span>为必填项
|
||||
</div>
|
||||
<div>3. 点击上传区域选择文件,或直接拖拽文件到上传区域</div>
|
||||
<div>4. 系统自动校验数据,可预览前10条数据及错误详情</div>
|
||||
<div>5. 选择导入策略(跳过/更新已存在),点击「开始导入」</div>
|
||||
</div>
|
||||
<div style={{ marginTop: '8px' }}><strong>字段说明:</strong></div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<strong>字段说明:</strong>
|
||||
</div>
|
||||
<div style={{ paddingLeft: '12px', marginTop: '4px' }}>
|
||||
<div>• <strong>设备ID</strong>(必填):服务器的唯一标识,如DEV001</div>
|
||||
<div>• <strong>网卡名称</strong>(必填):网卡的名称或标识,如eth0、网卡1</div>
|
||||
<div>• <strong>插槽编号</strong>(选填):网卡所在的插槽位置,必须为数字</div>
|
||||
<div>• <strong>网卡型号</strong>(选填):如Intel X710、BCM57414</div>
|
||||
<div>• <strong>制造商</strong>(选填):如Intel、Mellanox</div>
|
||||
<div>• <strong>描述</strong>(选填):备注信息</div>
|
||||
<div>
|
||||
• <strong>设备ID</strong>(必填):服务器的唯一标识,如DEV001
|
||||
</div>
|
||||
<div>
|
||||
• <strong>网卡名称</strong>(必填):网卡的名称或标识,如eth0、网卡1
|
||||
</div>
|
||||
<div>
|
||||
• <strong>插槽编号</strong>(选填):网卡所在的插槽位置,必须为数字
|
||||
</div>
|
||||
<div>
|
||||
• <strong>网卡型号</strong>(选填):如Intel X710、BCM57414
|
||||
</div>
|
||||
<div>
|
||||
• <strong>制造商</strong>(选填):如Intel、Mellanox
|
||||
</div>
|
||||
<div>
|
||||
• <strong>描述</strong>(选填):备注信息
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: '8px', color: '#faad14' }}>
|
||||
<strong>注意事项:</strong>
|
||||
</div>
|
||||
<div style={{ marginTop: '8px', color: '#faad14' }}><strong>注意事项:</strong></div>
|
||||
<div style={{ paddingLeft: '12px', marginTop: '4px', color: '#faad14' }}>
|
||||
<div>• 同一设备下网卡名称不可重复</div>
|
||||
<div>• 导入后需在网卡管理中为网卡添加端口</div>
|
||||
@@ -387,7 +417,10 @@ function NetworkCardImportModal({ visible, onClose, onSuccess }) {
|
||||
<p className="ant-upload-drag-icon">
|
||||
<UploadOutlined style={{ fontSize: '48px', color: designTokens.colors.primary.main }} />
|
||||
</p>
|
||||
<p className="ant-upload-text" style={{ fontSize: '16px', color: designTokens.colors.neutral[700] }}>
|
||||
<p
|
||||
className="ant-upload-text"
|
||||
style={{ fontSize: '16px', color: designTokens.colors.neutral[700] }}
|
||||
>
|
||||
点击或拖拽文件到此处上传
|
||||
</p>
|
||||
<p className="ant-upload-hint" style={{ color: designTokens.colors.neutral[500] }}>
|
||||
@@ -493,7 +526,13 @@ function NetworkCardImportModal({ visible, onClose, onSuccess }) {
|
||||
showIcon
|
||||
style={{ marginBottom: '16px', borderRadius: designTokens.borderRadius.md }}
|
||||
/>
|
||||
<div style={{ marginBottom: '8px', fontWeight: 500, color: designTokens.colors.neutral[700] }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '8px',
|
||||
fontWeight: 500,
|
||||
color: designTokens.colors.neutral[700],
|
||||
}}
|
||||
>
|
||||
数据预览(前10条)
|
||||
</div>
|
||||
<Table
|
||||
@@ -513,7 +552,13 @@ function NetworkCardImportModal({ visible, onClose, onSuccess }) {
|
||||
style={{ borderRadius: designTokens.borderRadius.md }}
|
||||
/>
|
||||
{importPreview.length > 10 && (
|
||||
<div style={{ textAlign: 'center', marginTop: '12px', color: designTokens.colors.neutral[500] }}>
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
marginTop: '12px',
|
||||
color: designTokens.colors.neutral[500],
|
||||
}}
|
||||
>
|
||||
仅显示前10条数据,共 {importPreview.length} 条
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -33,42 +33,52 @@ const PortAddGuideModal = ({ visible, onClose, onSelectType }) => {
|
||||
styles={{ body: { padding: 0 } }}
|
||||
destroyOnClose
|
||||
>
|
||||
<div style={{
|
||||
background: `linear-gradient(135deg, ${designTokens.colors.primary.main} 0%, ${designTokens.colors.primary.dark} 100%)`,
|
||||
padding: '28px 24px',
|
||||
borderRadius: '16px 16px 0 0',
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '14px',
|
||||
}}>
|
||||
<div style={{
|
||||
width: '52px',
|
||||
height: '52px',
|
||||
borderRadius: '14px',
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
<div
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${designTokens.colors.primary.main} 0%, ${designTokens.colors.primary.dark} 100%)`,
|
||||
padding: '28px 24px',
|
||||
borderRadius: '16px 16px 0 0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
}}>
|
||||
gap: '14px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '52px',
|
||||
height: '52px',
|
||||
borderRadius: '14px',
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
<SwapOutlined style={{ fontSize: '26px' }} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{
|
||||
margin: 0,
|
||||
color: '#fff',
|
||||
fontSize: '20px',
|
||||
fontWeight: 600,
|
||||
}}>
|
||||
<h3
|
||||
style={{
|
||||
margin: 0,
|
||||
color: '#fff',
|
||||
fontSize: '20px',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
选择端口类型
|
||||
</h3>
|
||||
<p style={{
|
||||
margin: '4px 0 0',
|
||||
color: 'rgba(255,255,255,0.85)',
|
||||
fontSize: '13px',
|
||||
}}>
|
||||
<p
|
||||
style={{
|
||||
margin: '4px 0 0',
|
||||
color: 'rgba(255,255,255,0.85)',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
>
|
||||
请选择要添加的端口类型
|
||||
</p>
|
||||
</div>
|
||||
@@ -76,12 +86,14 @@ const PortAddGuideModal = ({ visible, onClose, onSelectType }) => {
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '24px' }}>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '16px',
|
||||
marginBottom: '20px',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '16px',
|
||||
marginBottom: '20px',
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={handleSelectSwitch}
|
||||
style={{
|
||||
@@ -107,43 +119,51 @@ const PortAddGuideModal = ({ visible, onClose, onSelectType }) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: '56px',
|
||||
height: '56px',
|
||||
borderRadius: '14px',
|
||||
background: designTokens.colors.success.gradient,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 12px rgba(16, 185, 129, 0.3)',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: '56px',
|
||||
height: '56px',
|
||||
borderRadius: '14px',
|
||||
background: designTokens.colors.success.gradient,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 12px rgba(16, 185, 129, 0.3)',
|
||||
}}
|
||||
>
|
||||
<SwapOutlined style={{ fontSize: '28px' }} />
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 600,
|
||||
color: designTokens.colors.neutral[800],
|
||||
marginBottom: '4px',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 600,
|
||||
color: designTokens.colors.neutral[800],
|
||||
marginBottom: '4px',
|
||||
}}
|
||||
>
|
||||
交换机端口
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: designTokens.colors.neutral[500],
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: designTokens.colors.neutral[500],
|
||||
}}
|
||||
>
|
||||
直接添加端口
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
color: designTokens.colors.success.main,
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
color: designTokens.colors.success.main,
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<span>立即添加</span>
|
||||
<ArrowRightOutlined style={{ fontSize: '11px' }} />
|
||||
</div>
|
||||
@@ -174,43 +194,51 @@ const PortAddGuideModal = ({ visible, onClose, onSelectType }) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: '56px',
|
||||
height: '56px',
|
||||
borderRadius: '14px',
|
||||
background: designTokens.colors.primary.gradient,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: '56px',
|
||||
height: '56px',
|
||||
borderRadius: '14px',
|
||||
background: designTokens.colors.primary.gradient,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)',
|
||||
}}
|
||||
>
|
||||
<CloudServerOutlined style={{ fontSize: '28px' }} />
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 600,
|
||||
color: designTokens.colors.neutral[800],
|
||||
marginBottom: '4px',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 600,
|
||||
color: designTokens.colors.neutral[800],
|
||||
marginBottom: '4px',
|
||||
}}
|
||||
>
|
||||
服务器端口
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: designTokens.colors.neutral[500],
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: designTokens.colors.neutral[500],
|
||||
}}
|
||||
>
|
||||
需关联网卡
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
color: designTokens.colors.primary.main,
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
color: designTokens.colors.primary.main,
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<span>立即添加</span>
|
||||
<ArrowRightOutlined style={{ fontSize: '11px' }} />
|
||||
</div>
|
||||
@@ -218,70 +246,88 @@ const PortAddGuideModal = ({ visible, onClose, onSelectType }) => {
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: '0 0 20px' }}>
|
||||
<span style={{
|
||||
fontSize: '12px',
|
||||
color: designTokens.colors.neutral[400],
|
||||
fontWeight: 400,
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: designTokens.colors.neutral[400],
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
端口类型说明
|
||||
</span>
|
||||
</Divider>
|
||||
|
||||
<div style={{
|
||||
background: designTokens.colors.neutral[50],
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
border: `1px solid ${designTokens.colors.neutral[200]}`,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
background: designTokens.colors.neutral[50],
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
border: `1px solid ${designTokens.colors.neutral[200]}`,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '10px' }}>
|
||||
<CheckCircleOutlined style={{
|
||||
color: designTokens.colors.success.main,
|
||||
fontSize: '16px',
|
||||
marginTop: '2px',
|
||||
flexShrink: 0,
|
||||
}} />
|
||||
<CheckCircleOutlined
|
||||
style={{
|
||||
color: designTokens.colors.success.main,
|
||||
fontSize: '16px',
|
||||
marginTop: '2px',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<div style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: 600,
|
||||
color: designTokens.colors.neutral[800],
|
||||
marginBottom: '2px',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: 600,
|
||||
color: designTokens.colors.neutral[800],
|
||||
marginBottom: '2px',
|
||||
}}
|
||||
>
|
||||
交换机端口
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: designTokens.colors.neutral[500],
|
||||
lineHeight: 1.6,
|
||||
}}>
|
||||
交换机端口用于网络设备间的连接,可以直接创建端口,无需关联网卡。适用于创建 Uplink 端口、Trunk 端口等。
|
||||
<div
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: designTokens.colors.neutral[500],
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
交换机端口用于网络设备间的连接,可以直接创建端口,无需关联网卡。适用于创建 Uplink
|
||||
端口、Trunk 端口等。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '10px' }}>
|
||||
<ExclamationCircleOutlined style={{
|
||||
color: designTokens.colors.warning.main,
|
||||
fontSize: '16px',
|
||||
marginTop: '2px',
|
||||
flexShrink: 0,
|
||||
}} />
|
||||
<ExclamationCircleOutlined
|
||||
style={{
|
||||
color: designTokens.colors.warning.main,
|
||||
fontSize: '16px',
|
||||
marginTop: '2px',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<div style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: 600,
|
||||
color: designTokens.colors.neutral[800],
|
||||
marginBottom: '2px',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: 600,
|
||||
color: designTokens.colors.neutral[800],
|
||||
marginBottom: '2px',
|
||||
}}
|
||||
>
|
||||
服务器端口
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: designTokens.colors.neutral[500],
|
||||
lineHeight: 1.6,
|
||||
}}>
|
||||
服务器端口必须关联网卡(Network Card),每个端口需要对应一个物理或虚拟网卡。请先在网卡管理中添加网卡。
|
||||
<div
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: designTokens.colors.neutral[500],
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
服务器端口必须关联网卡(Network
|
||||
Card),每个端口需要对应一个物理或虚拟网卡。请先在网卡管理中添加网卡。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -292,7 +338,8 @@ const PortAddGuideModal = ({ visible, onClose, onSelectType }) => {
|
||||
message=""
|
||||
description={
|
||||
<div style={{ fontSize: '12px', lineHeight: 1.6 }}>
|
||||
<strong>提示:</strong>如果服务器尚未添加网卡,系统会引导您先前往网卡管理添加网卡后再创建端口。
|
||||
<strong>提示:</strong>
|
||||
如果服务器尚未添加网卡,系统会引导您先前往网卡管理添加网卡后再创建端口。
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
@@ -306,11 +353,13 @@ const PortAddGuideModal = ({ visible, onClose, onSelectType }) => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
marginTop: '20px',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
marginTop: '20px',
|
||||
}}
|
||||
>
|
||||
<Button onClick={onClose} style={{ borderRadius: '8px' }}>
|
||||
取消
|
||||
</Button>
|
||||
|
||||
@@ -91,7 +91,16 @@ function generatePortNames(portName) {
|
||||
return [portName];
|
||||
}
|
||||
|
||||
function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, networkCards = [], networkCard, disableNicChange = false }) {
|
||||
function PortCreateModal({
|
||||
device,
|
||||
visible,
|
||||
onClose,
|
||||
onSuccess,
|
||||
defaultNicId,
|
||||
networkCards = [],
|
||||
networkCard,
|
||||
disableNicChange = false,
|
||||
}) {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [previewPorts, setPreviewPorts] = useState([]);
|
||||
@@ -153,7 +162,7 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
||||
setLoading(true);
|
||||
|
||||
const portNames = generatePortNames(values.portName);
|
||||
const finalNicId = disableNicChange && defaultNicId ? defaultNicId : (values.nicId || null);
|
||||
const finalNicId = disableNicChange && defaultNicId ? defaultNicId : values.nicId || null;
|
||||
|
||||
if (portNames.length === 1) {
|
||||
await axios.post('/api/device-ports', {
|
||||
@@ -166,7 +175,10 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
||||
status: values.status,
|
||||
description: values.description,
|
||||
});
|
||||
message.success({ content: '端口创建成功', icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} /> });
|
||||
message.success({
|
||||
content: '端口创建成功',
|
||||
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
|
||||
});
|
||||
} else {
|
||||
const portsData = portNames.map((portName, index) => ({
|
||||
portId: `PORT-${Date.now()}-${index}`,
|
||||
@@ -181,7 +193,10 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
||||
}));
|
||||
|
||||
await axios.post('/api/device-ports/batch', { ports: portsData });
|
||||
message.success({ content: `成功创建 ${portNames.length} 个端口`, icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} /> });
|
||||
message.success({
|
||||
content: `成功创建 ${portNames.length} 个端口`,
|
||||
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
|
||||
});
|
||||
}
|
||||
|
||||
form.resetFields();
|
||||
@@ -207,7 +222,7 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
||||
onClose();
|
||||
}, [form, onClose]);
|
||||
|
||||
const handleValuesChange = (changedValues) => {
|
||||
const handleValuesChange = changedValues => {
|
||||
if (changedValues.portName) {
|
||||
handlePortNameChange({ target: { value: changedValues.portName } });
|
||||
}
|
||||
@@ -411,266 +426,365 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
||||
>
|
||||
<Alert
|
||||
message="格式说明"
|
||||
description={
|
||||
<div style={{ fontSize: '11px', lineHeight: '1.6' }}>
|
||||
<div>• <strong>单个端口:</strong>eth0/1、gigabitethernet1/0/1</div>
|
||||
<div>• <strong>端口范围:</strong>1/0/1-1/0/48(创建 1/0/1 到 1/0/48 共48个端口)</div>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ ...styles.alertBox, marginBottom: '16px' }}
|
||||
/>
|
||||
|
||||
<Row gutter={20} style={{ marginBottom: '16px' }}>
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||
<div style={{ ...styles.section, height: '100%' }}>
|
||||
<div style={styles.sectionTitle}>
|
||||
<TagOutlined style={{ color: designTokens.colors.primary.main }} />
|
||||
端口标识
|
||||
description={
|
||||
<div style={{ fontSize: '11px', lineHeight: '1.6' }}>
|
||||
<div>
|
||||
• <strong>单个端口:</strong>eth0/1、gigabitethernet1/0/1
|
||||
</div>
|
||||
<div>
|
||||
• <strong>端口范围:</strong>1/0/1-1/0/48(创建 1/0/1 到 1/0/48 共48个端口)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ ...styles.alertBox, marginBottom: '16px' }}
|
||||
/>
|
||||
|
||||
<Form.Item
|
||||
name="portName"
|
||||
rules={[
|
||||
{ required: true, message: '请输入端口名称' },
|
||||
{
|
||||
pattern: /^[\w\/:\-]+$/,
|
||||
message: '端口名称格式不正确',
|
||||
},
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (!value) return Promise.resolve();
|
||||
const ports = generatePortNames(value);
|
||||
if (ports.length > 1000) {
|
||||
return Promise.reject(new Error('单次最多创建1000个端口'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
<Row gutter={20} style={{ marginBottom: '16px' }}>
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||
<div style={{ ...styles.section, height: '100%' }}>
|
||||
<div style={styles.sectionTitle}>
|
||||
<TagOutlined style={{ color: designTokens.colors.primary.main }} />
|
||||
端口标识
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
name="portName"
|
||||
rules={[
|
||||
{ required: true, message: '请输入端口名称' },
|
||||
{
|
||||
pattern: /^[\w\/:\-]+$/,
|
||||
message: '端口名称格式不正确',
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
size="small"
|
||||
placeholder="例如: eth0/1 或 1/0/1-1/0/48"
|
||||
prefix={<TagOutlined style={{ color: designTokens.colors.neutral[400], fontSize: '12px' }} />}
|
||||
style={{ borderRadius: '6px' }}
|
||||
suffix={
|
||||
<Tooltip title="支持单个端口或端口范围(如 1/0/1-1/0/48)">
|
||||
<InfoCircleOutlined style={{ color: designTokens.colors.neutral[400], fontSize: '11px' }} />
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (!value) return Promise.resolve();
|
||||
const ports = generatePortNames(value);
|
||||
if (ports.length > 1000) {
|
||||
return Promise.reject(new Error('单次最多创建1000个端口'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
size="small"
|
||||
placeholder="例如: eth0/1 或 1/0/1-1/0/48"
|
||||
prefix={
|
||||
<TagOutlined
|
||||
style={{ color: designTokens.colors.neutral[400], fontSize: '12px' }}
|
||||
/>
|
||||
}
|
||||
style={{ borderRadius: '6px' }}
|
||||
suffix={
|
||||
<Tooltip title="支持单个端口或端口范围(如 1/0/1-1/0/48)">
|
||||
<InfoCircleOutlined
|
||||
style={{ color: designTokens.colors.neutral[400], fontSize: '11px' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{showPreview && (
|
||||
<div style={styles.previewCard}>
|
||||
<div style={styles.previewTitle}>
|
||||
<ThunderboltOutlined />
|
||||
将创建 {previewPorts.length} 个端口
|
||||
</div>
|
||||
<div style={styles.previewTags}>
|
||||
{previewPorts.map((port, index) => (
|
||||
<Tag key={index} style={styles.previewTag}>
|
||||
{port}
|
||||
</Tag>
|
||||
))}
|
||||
{parsePortRange(form.getFieldValue('portName'))?.portCount > previewPorts.length && (
|
||||
<Tag style={{ ...styles.previewTag, background: designTokens.colors.neutral[100] }}>
|
||||
...等 {parsePortRange(form.getFieldValue('portName'))?.portCount} 个
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||
<div style={{ ...styles.section, height: '100%' }}>
|
||||
<div style={styles.sectionTitle}>
|
||||
<LinkOutlined style={{ color: designTokens.colors.primary.main }} />
|
||||
网卡关联
|
||||
</div>
|
||||
|
||||
{disableNicChange && defaultNicId ? (
|
||||
<div style={{ ...styles.nicSelectedCard, background: designTokens.colors.success.bg, border: `1px solid ${designTokens.colors.success.light}` }}>
|
||||
<div style={{ ...styles.nicSelectedIcon, color: designTokens.colors.success.main }}>
|
||||
<CheckCircleOutlined />
|
||||
</div>
|
||||
<div style={styles.nicSelectedInfo}>
|
||||
<div style={{ ...styles.nicSelectedName, color: designTokens.colors.success.dark }}>
|
||||
{nicList.find(nic => nic.nicId === defaultNicId)?.name || '管理口'}
|
||||
{showPreview && (
|
||||
<div style={styles.previewCard}>
|
||||
<div style={styles.previewTitle}>
|
||||
<ThunderboltOutlined />
|
||||
将创建 {previewPorts.length} 个端口
|
||||
</div>
|
||||
{nicList.find(nic => nic.nicId === defaultNicId)?.slotNumber && (
|
||||
<div style={{ ...styles.nicSelectedSlot, color: designTokens.colors.success.main }}>
|
||||
插槽 {nicList.find(nic => nic.nicId === defaultNicId)?.slotNumber}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Tag color="success">已绑定</Tag>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Form.Item name="nicId" style={{ marginBottom: 0 }}>
|
||||
<Select
|
||||
size="small"
|
||||
placeholder="选择网卡(可选)"
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
style={{ width: '100%', borderRadius: '6px' }}
|
||||
suffixIcon={<InfoCircleOutlined style={{ color: designTokens.colors.neutral[400] }} />}
|
||||
>
|
||||
{nicList.map(nic => (
|
||||
<Option key={nic.nicId} value={nic.nicId}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '2px 0' }}>
|
||||
<div>
|
||||
<div style={{ fontWeight: 500, fontSize: '12px' }}>{nic.name}</div>
|
||||
{nic.slotNumber && (
|
||||
<div style={{ fontSize: '11px', color: designTokens.colors.neutral[500] }}>
|
||||
插槽 {nic.slotNumber}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Option>
|
||||
<div style={styles.previewTags}>
|
||||
{previewPorts.map((port, index) => (
|
||||
<Tag key={index} style={styles.previewTag}>
|
||||
{port}
|
||||
</Tag>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ fontSize: '11px', color: designTokens.colors.neutral[500], marginTop: '6px' }}>
|
||||
<InfoCircleOutlined style={{ marginRight: '4px' }} />
|
||||
不选择则端口不归属于任何网卡
|
||||
{parsePortRange(form.getFieldValue('portName'))?.portCount >
|
||||
previewPorts.length && (
|
||||
<Tag
|
||||
style={{
|
||||
...styles.previewTag,
|
||||
background: designTokens.colors.neutral[100],
|
||||
}}
|
||||
>
|
||||
...等 {parsePortRange(form.getFieldValue('portName'))?.portCount} 个
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||
<div style={{ ...styles.section, height: '100%' }}>
|
||||
<div style={styles.sectionTitle}>
|
||||
<LinkOutlined style={{ color: designTokens.colors.primary.main }} />
|
||||
网卡关联
|
||||
</div>
|
||||
|
||||
{disableNicChange && defaultNicId ? (
|
||||
<div
|
||||
style={{
|
||||
...styles.nicSelectedCard,
|
||||
background: designTokens.colors.success.bg,
|
||||
border: `1px solid ${designTokens.colors.success.light}`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ ...styles.nicSelectedIcon, color: designTokens.colors.success.main }}
|
||||
>
|
||||
<CheckCircleOutlined />
|
||||
</div>
|
||||
<div style={styles.nicSelectedInfo}>
|
||||
<div
|
||||
style={{
|
||||
...styles.nicSelectedName,
|
||||
color: designTokens.colors.success.dark,
|
||||
}}
|
||||
>
|
||||
{nicList.find(nic => nic.nicId === defaultNicId)?.name || '管理口'}
|
||||
</div>
|
||||
{nicList.find(nic => nic.nicId === defaultNicId)?.slotNumber && (
|
||||
<div
|
||||
style={{
|
||||
...styles.nicSelectedSlot,
|
||||
color: designTokens.colors.success.main,
|
||||
}}
|
||||
>
|
||||
插槽 {nicList.find(nic => nic.nicId === defaultNicId)?.slotNumber}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Tag color="success">已绑定</Tag>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Form.Item name="nicId" style={{ marginBottom: 0 }}>
|
||||
<Select
|
||||
size="small"
|
||||
placeholder="选择网卡(可选)"
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
style={{ width: '100%', borderRadius: '6px' }}
|
||||
suffixIcon={
|
||||
<InfoCircleOutlined style={{ color: designTokens.colors.neutral[400] }} />
|
||||
}
|
||||
>
|
||||
{nicList.map(nic => (
|
||||
<Option key={nic.nicId} value={nic.nicId}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '2px 0',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div style={{ fontWeight: 500, fontSize: '12px' }}>{nic.name}</div>
|
||||
{nic.slotNumber && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: designTokens.colors.neutral[500],
|
||||
}}
|
||||
>
|
||||
插槽 {nic.slotNumber}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: designTokens.colors.neutral[500],
|
||||
marginTop: '6px',
|
||||
}}
|
||||
>
|
||||
<InfoCircleOutlined style={{ marginRight: '4px' }} />
|
||||
不选择则端口不归属于任何网卡
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div style={{ ...styles.section, marginBottom: '16px' }}>
|
||||
<div style={styles.sectionTitle}>
|
||||
<ThunderboltOutlined style={{ color: designTokens.colors.primary.main }} />
|
||||
端口属性
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div style={{ ...styles.section, marginBottom: '16px' }}>
|
||||
<div style={styles.sectionTitle}>
|
||||
<ThunderboltOutlined style={{ color: designTokens.colors.primary.main }} />
|
||||
端口属性
|
||||
<Row gutter={12}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="portType"
|
||||
label={<span style={styles.fieldLabel}>端口类型</span>}
|
||||
rules={[{ required: true, message: '请选择' }]}
|
||||
>
|
||||
<Select size="small" style={{ width: '100%', borderRadius: '6px' }}>
|
||||
<Option value="RJ45">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '6px',
|
||||
height: '6px',
|
||||
borderRadius: '2px',
|
||||
background: designTokens.colors.device.server,
|
||||
}}
|
||||
/>
|
||||
RJ45
|
||||
</div>
|
||||
</Option>
|
||||
<Option value="SFP">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '6px',
|
||||
height: '6px',
|
||||
borderRadius: '2px',
|
||||
background: designTokens.colors.device.switch,
|
||||
}}
|
||||
/>
|
||||
SFP
|
||||
</div>
|
||||
</Option>
|
||||
<Option value="SFP+">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '6px',
|
||||
height: '6px',
|
||||
borderRadius: '2px',
|
||||
background: designTokens.colors.purple.main,
|
||||
}}
|
||||
/>
|
||||
SFP+
|
||||
</div>
|
||||
</Option>
|
||||
<Option value="SFP28">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '6px',
|
||||
height: '6px',
|
||||
borderRadius: '2px',
|
||||
background: designTokens.colors.info.main,
|
||||
}}
|
||||
/>
|
||||
SFP28
|
||||
</div>
|
||||
</Option>
|
||||
<Option value="QSFP">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '6px',
|
||||
height: '6px',
|
||||
borderRadius: '2px',
|
||||
background: designTokens.colors.warning.main,
|
||||
}}
|
||||
/>
|
||||
QSFP
|
||||
</div>
|
||||
</Option>
|
||||
<Option value="QSFP28">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '6px',
|
||||
height: '6px',
|
||||
borderRadius: '2px',
|
||||
background: designTokens.colors.secondary.main,
|
||||
}}
|
||||
/>
|
||||
QSFP28
|
||||
</div>
|
||||
</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="portSpeed"
|
||||
label={<span style={styles.fieldLabel}>端口速率</span>}
|
||||
rules={[{ required: true, message: '请选择' }]}
|
||||
>
|
||||
<Select size="small" style={{ width: '100%', borderRadius: '6px' }}>
|
||||
<Option value="100M">100M</Option>
|
||||
<Option value="1G">1G</Option>
|
||||
<Option value="10G">10G</Option>
|
||||
<Option value="25G">25G</Option>
|
||||
<Option value="40G">40G</Option>
|
||||
<Option value="100G">100G</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={12}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="vlanId" label={<span style={styles.fieldLabel}>VLAN ID</span>}>
|
||||
<Input
|
||||
size="small"
|
||||
placeholder="1-4094"
|
||||
style={{ width: '100%', borderRadius: '6px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="status"
|
||||
label={<span style={styles.fieldLabel}>状态</span>}
|
||||
rules={[{ required: true, message: '请选择' }]}
|
||||
>
|
||||
<Select size="small" style={{ width: '100%', borderRadius: '6px' }}>
|
||||
<Option value="free">
|
||||
<Tag color="success" style={{ margin: 0 }}>
|
||||
空闲
|
||||
</Tag>
|
||||
</Option>
|
||||
<Option value="occupied">
|
||||
<Tag color="warning" style={{ margin: 0 }}>
|
||||
占用
|
||||
</Tag>
|
||||
</Option>
|
||||
<Option value="fault">
|
||||
<Tag color="error" style={{ margin: 0 }}>
|
||||
故障
|
||||
</Tag>
|
||||
</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<Row gutter={12}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="portType"
|
||||
label={<span style={styles.fieldLabel}>端口类型</span>}
|
||||
rules={[{ required: true, message: '请选择' }]}
|
||||
>
|
||||
<Select size="small" style={{ width: '100%', borderRadius: '6px' }}>
|
||||
<Option value="RJ45">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<div style={{ width: '6px', height: '6px', borderRadius: '2px', background: designTokens.colors.device.server }} />
|
||||
RJ45
|
||||
</div>
|
||||
</Option>
|
||||
<Option value="SFP">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<div style={{ width: '6px', height: '6px', borderRadius: '2px', background: designTokens.colors.device.switch }} />
|
||||
SFP
|
||||
</div>
|
||||
</Option>
|
||||
<Option value="SFP+">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<div style={{ width: '6px', height: '6px', borderRadius: '2px', background: designTokens.colors.purple.main }} />
|
||||
SFP+
|
||||
</div>
|
||||
</Option>
|
||||
<Option value="SFP28">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<div style={{ width: '6px', height: '6px', borderRadius: '2px', background: designTokens.colors.info.main }} />
|
||||
SFP28
|
||||
</div>
|
||||
</Option>
|
||||
<Option value="QSFP">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<div style={{ width: '6px', height: '6px', borderRadius: '2px', background: designTokens.colors.warning.main }} />
|
||||
QSFP
|
||||
</div>
|
||||
</Option>
|
||||
<Option value="QSFP28">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<div style={{ width: '6px', height: '6px', borderRadius: '2px', background: designTokens.colors.secondary.main }} />
|
||||
QSFP28
|
||||
</div>
|
||||
</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<div style={styles.section}>
|
||||
<div style={styles.sectionTitle}>
|
||||
<FileTextOutlined style={{ color: designTokens.colors.primary.main }} />
|
||||
描述信息
|
||||
</div>
|
||||
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="portSpeed"
|
||||
label={<span style={styles.fieldLabel}>端口速率</span>}
|
||||
rules={[{ required: true, message: '请选择' }]}
|
||||
>
|
||||
<Select size="small" style={{ width: '100%', borderRadius: '6px' }}>
|
||||
<Option value="100M">100M</Option>
|
||||
<Option value="1G">1G</Option>
|
||||
<Option value="10G">10G</Option>
|
||||
<Option value="25G">25G</Option>
|
||||
<Option value="40G">40G</Option>
|
||||
<Option value="100G">100G</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={12}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="vlanId"
|
||||
label={<span style={styles.fieldLabel}>VLAN ID</span>}
|
||||
>
|
||||
<Input
|
||||
size="small"
|
||||
placeholder="1-4094"
|
||||
style={{ width: '100%', borderRadius: '6px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="status"
|
||||
label={<span style={styles.fieldLabel}>状态</span>}
|
||||
rules={[{ required: true, message: '请选择' }]}
|
||||
>
|
||||
<Select size="small" style={{ width: '100%', borderRadius: '6px' }}>
|
||||
<Option value="free">
|
||||
<Tag color="success" style={{ margin: 0 }}>空闲</Tag>
|
||||
</Option>
|
||||
<Option value="occupied">
|
||||
<Tag color="warning" style={{ margin: 0 }}>占用</Tag>
|
||||
</Option>
|
||||
<Option value="fault">
|
||||
<Tag color="error" style={{ margin: 0 }}>故障</Tag>
|
||||
</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<div style={styles.section}>
|
||||
<div style={styles.sectionTitle}>
|
||||
<FileTextOutlined style={{ color: designTokens.colors.primary.main }} />
|
||||
描述信息
|
||||
<Form.Item name="description" style={{ marginBottom: 0 }}>
|
||||
<TextArea
|
||||
rows={2}
|
||||
placeholder="请输入描述信息(可选)"
|
||||
style={{ borderRadius: '6px', resize: 'none' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item name="description" style={{ marginBottom: 0 }}>
|
||||
<TextArea
|
||||
rows={2}
|
||||
placeholder="请输入描述信息(可选)"
|
||||
style={{ borderRadius: '6px', resize: 'none' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
@@ -678,7 +792,9 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
||||
<div style={styles.footerLeft}>
|
||||
{portCount > 0 && (
|
||||
<span>
|
||||
将创建 <strong style={{ color: designTokens.colors.primary.main }}>{portCount}</strong> 个端口
|
||||
将创建{' '}
|
||||
<strong style={{ color: designTokens.colors.primary.main }}>{portCount}</strong>{' '}
|
||||
个端口
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -706,4 +822,4 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(PortCreateModal);
|
||||
export default React.memo(PortCreateModal);
|
||||
|
||||
@@ -57,8 +57,10 @@ const PortExportModal = ({
|
||||
const activeFilters = [];
|
||||
if (filters.deviceId) activeFilters.push(`设备ID: ${filters.deviceId}`);
|
||||
if (filters.status && filters.status !== 'all') activeFilters.push(`状态: ${filters.status}`);
|
||||
if (filters.portType && filters.portType !== 'all') activeFilters.push(`类型: ${filters.portType}`);
|
||||
if (filters.portSpeed && filters.portSpeed !== 'all') activeFilters.push(`速率: ${filters.portSpeed}`);
|
||||
if (filters.portType && filters.portType !== 'all')
|
||||
activeFilters.push(`类型: ${filters.portType}`);
|
||||
if (filters.portSpeed && filters.portSpeed !== 'all')
|
||||
activeFilters.push(`速率: ${filters.portSpeed}`);
|
||||
if (filters.searchText) activeFilters.push(`搜索: ${filters.searchText}`);
|
||||
return activeFilters;
|
||||
};
|
||||
@@ -142,15 +144,18 @@ const PortExportModal = ({
|
||||
</Text>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
{activeFilters.map((filter, index) => (
|
||||
<span key={index} style={{
|
||||
display: 'inline-block',
|
||||
background: designTokens.colors.neutral[100],
|
||||
padding: '2px 8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
marginRight: '4px',
|
||||
marginBottom: '4px',
|
||||
}}>
|
||||
<span
|
||||
key={index}
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
background: designTokens.colors.neutral[100],
|
||||
padding: '2px 8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
marginRight: '4px',
|
||||
marginBottom: '4px',
|
||||
}}
|
||||
>
|
||||
{filter}
|
||||
</span>
|
||||
))}
|
||||
@@ -164,7 +169,8 @@ const PortExportModal = ({
|
||||
message={
|
||||
<div>
|
||||
<Text style={{ fontSize: '13px' }}>
|
||||
导出将包含以下字段:端口ID、设备ID、设备名称、设备类型、机房、机架、网卡名称、端口名称、端口类型、端口速率、状态、VLAN ID、描述、创建时间
|
||||
导出将包含以下字段:端口ID、设备ID、设备名称、设备类型、机房、机架、网卡名称、端口名称、端口类型、端口速率、状态、VLAN
|
||||
ID、描述、创建时间
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
@@ -176,7 +182,8 @@ const PortExportModal = ({
|
||||
type="warning"
|
||||
message={
|
||||
<Text style={{ fontSize: '13px' }}>
|
||||
当前数据量较大({totalCount} 条),导出可能需要较长时间。系统最大支持导出 50000 条数据。
|
||||
当前数据量较大({totalCount} 条),导出可能需要较长时间。系统最大支持导出 50000
|
||||
条数据。
|
||||
</Text>
|
||||
}
|
||||
style={{ marginTop: '12px' }}
|
||||
@@ -187,4 +194,4 @@ const PortExportModal = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(PortExportModal);
|
||||
export default React.memo(PortExportModal);
|
||||
|
||||
@@ -49,7 +49,7 @@ function ServerNicCard({ server, onManage }) {
|
||||
const typeConfig = DEVICE_TYPE_CONFIG[server.type] || DEVICE_TYPE_CONFIG.other;
|
||||
const statusConfig = STATUS_CONFIG[server.status] || STATUS_CONFIG.offline;
|
||||
|
||||
const nicCount = server.nicCount || (server.nics?.length || 0);
|
||||
const nicCount = server.nicCount || server.nics?.length || 0;
|
||||
const totalPortCount = server.nics?.reduce((sum, nic) => sum + (nic.portCount || 0), 0) || 0;
|
||||
|
||||
return (
|
||||
@@ -170,20 +170,26 @@ function ServerNicCard({ server, onManage }) {
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '18px', fontWeight: 700, color: designTokens.colors.primary.main }}>
|
||||
<div
|
||||
style={{ fontSize: '18px', fontWeight: 700, color: designTokens.colors.primary.main }}
|
||||
>
|
||||
{nicCount}
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: designTokens.colors.text.secondary }}>
|
||||
网卡
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: designTokens.colors.text.secondary }}>网卡</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', borderLeft: `1px solid ${designTokens.colors.border.light}`, borderRight: `1px solid ${designTokens.colors.border.light}` }}>
|
||||
<div style={{ fontSize: '18px', fontWeight: 700, color: designTokens.colors.info.main }}>
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
borderLeft: `1px solid ${designTokens.colors.border.light}`,
|
||||
borderRight: `1px solid ${designTokens.colors.border.light}`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ fontSize: '18px', fontWeight: 700, color: designTokens.colors.info.main }}
|
||||
>
|
||||
{totalPortCount}
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: designTokens.colors.text.secondary }}>
|
||||
端口
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: designTokens.colors.text.secondary }}>端口</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Badge
|
||||
@@ -211,7 +217,7 @@ function ServerNicCard({ server, onManage }) {
|
||||
color: designTokens.colors.primary.main,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onManage();
|
||||
}}
|
||||
|
||||
@@ -6,7 +6,7 @@ const AnimatedCounter = ({ value, duration = 1500 }) => {
|
||||
const startTimeRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const animate = (currentTime) => {
|
||||
const animate = currentTime => {
|
||||
if (!startTimeRef.current) {
|
||||
startTimeRef.current = currentTime;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { designTokens } from '../../config/theme';
|
||||
|
||||
const DeviceTrendChart = ({ data }) => {
|
||||
const maxValue = Math.max(...data.map((d) => d.value));
|
||||
const maxValue = Math.max(...data.map(d => d.value));
|
||||
const chartHeight = 120;
|
||||
|
||||
return (
|
||||
|
||||
@@ -72,7 +72,7 @@ const createNavButtonStyle = (color, isHovered) => ({
|
||||
minWidth: 0,
|
||||
});
|
||||
|
||||
const createNavIconContainer = (color) => ({
|
||||
const createNavIconContainer = color => ({
|
||||
width: 'clamp(44px, 10vw, 60px)',
|
||||
height: 'clamp(44px, 10vw, 60px)',
|
||||
borderRadius: designTokens.borderRadius.medium,
|
||||
@@ -117,7 +117,7 @@ const NavigationGrid = ({ hoveredCard, onHover }) => {
|
||||
className="nav-button"
|
||||
style={{
|
||||
...createNavButtonStyle(color, isHovered),
|
||||
animationDelay: `${NAV_BUTTONS_DATA.findIndex((b) => b.key === key) * 0.1}s`,
|
||||
animationDelay: `${NAV_BUTTONS_DATA.findIndex(b => b.key === key) * 0.1}s`,
|
||||
}}
|
||||
onMouseEnter={() => onHover(`nav-${key}`)}
|
||||
onMouseLeave={() => onHover(null)}
|
||||
|
||||
@@ -17,9 +17,8 @@ const quickStatItemStyle = {
|
||||
};
|
||||
|
||||
const QuickStats = ({ onlineRate, powerUsage, totalMaxPower }) => {
|
||||
const powerUsagePercent = totalMaxPower > 0
|
||||
? ((powerUsage / totalMaxPower) * 100).toFixed(1)
|
||||
: '0.0';
|
||||
const powerUsagePercent =
|
||||
totalMaxPower > 0 ? ((powerUsage / totalMaxPower) * 100).toFixed(1) : '0.0';
|
||||
|
||||
const quickStats = [
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
|
||||
import { designTokens } from '../../config/theme';
|
||||
import AnimatedCounter from './AnimatedCounter';
|
||||
|
||||
const createStatCardStyle = (color) => ({
|
||||
const createStatCardStyle = color => ({
|
||||
borderRadius: designTokens.borderRadius.large,
|
||||
border: 'none',
|
||||
boxShadow: designTokens.shadows.medium,
|
||||
@@ -17,7 +17,7 @@ const createStatCardStyle = (color) => ({
|
||||
borderLeft: `4px solid ${color}`,
|
||||
});
|
||||
|
||||
const createStatIconContainer = (color) => ({
|
||||
const createStatIconContainer = color => ({
|
||||
width: 'clamp(40px, 8vw, 64px)',
|
||||
height: 'clamp(40px, 8vw, 64px)',
|
||||
borderRadius: designTokens.borderRadius.medium,
|
||||
@@ -30,14 +30,7 @@ const createStatIconContainer = (color) => ({
|
||||
background: `linear-gradient(135deg, ${color}20 0%, ${color}10 100%)`,
|
||||
});
|
||||
|
||||
const StatCard = ({
|
||||
config,
|
||||
stats,
|
||||
loading,
|
||||
animatedKey,
|
||||
hoveredCard,
|
||||
onHover,
|
||||
}) => {
|
||||
const StatCard = ({ config, stats, loading, animatedKey, hoveredCard, onHover }) => {
|
||||
const {
|
||||
icon: Icon,
|
||||
color,
|
||||
@@ -143,7 +136,8 @@ const StatCard = ({
|
||||
alignItems: 'center',
|
||||
fontSize: 'clamp(0.7rem, 1.8vw, 0.875rem)',
|
||||
fontWeight: '500',
|
||||
color: trend > 0 ? designTokens.colors.success.main : designTokens.colors.error.main,
|
||||
color:
|
||||
trend > 0 ? designTokens.colors.success.main : designTokens.colors.error.main,
|
||||
flexWrap: 'wrap',
|
||||
gap: '4px',
|
||||
}}
|
||||
|
||||
@@ -6,10 +6,26 @@ const { Text } = Typography;
|
||||
|
||||
const StatusLegend = ({ deviceStatusPercentages }) => {
|
||||
const legends = [
|
||||
{ color: designTokens.colors.success.main, label: '运行中', percent: deviceStatusPercentages?.running ?? 0 },
|
||||
{ color: designTokens.colors.warning.main, label: '维护中', percent: deviceStatusPercentages?.maintenance ?? 0 },
|
||||
{ color: designTokens.colors.error.main, label: '故障', percent: deviceStatusPercentages?.fault ?? 0 },
|
||||
{ color: designTokens.colors.primary.main, label: '离线', percent: deviceStatusPercentages?.offline ?? 0 },
|
||||
{
|
||||
color: designTokens.colors.success.main,
|
||||
label: '运行中',
|
||||
percent: deviceStatusPercentages?.running ?? 0,
|
||||
},
|
||||
{
|
||||
color: designTokens.colors.warning.main,
|
||||
label: '维护中',
|
||||
percent: deviceStatusPercentages?.maintenance ?? 0,
|
||||
},
|
||||
{
|
||||
color: designTokens.colors.error.main,
|
||||
label: '故障',
|
||||
percent: deviceStatusPercentages?.fault ?? 0,
|
||||
},
|
||||
{
|
||||
color: designTokens.colors.primary.main,
|
||||
label: '离线',
|
||||
percent: deviceStatusPercentages?.offline ?? 0,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -20,13 +20,7 @@ const secondaryActionStyle = {
|
||||
fontWeight: '500',
|
||||
};
|
||||
|
||||
const BatchStatusModal = ({
|
||||
visible,
|
||||
selectedCount,
|
||||
loading,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}) => {
|
||||
const BatchStatusModal = ({ visible, selectedCount, loading, onSubmit, onCancel }) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@@ -99,8 +93,7 @@ const BatchStatusModal = ({
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<div style={{ color: '#666', fontSize: '13px' }}>
|
||||
已选择{' '}
|
||||
<span style={{ color: '#1890ff', fontWeight: 600 }}>{selectedCount}</span> 个设备
|
||||
已选择 <span style={{ color: '#1890ff', fontWeight: 600 }}>{selectedCount}</span> 个设备
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
@@ -44,7 +44,7 @@ const DeviceDetailModal = ({
|
||||
|
||||
if (!device) return null;
|
||||
|
||||
const getStatusIcon = (status) => {
|
||||
const getStatusIcon = status => {
|
||||
const iconMap = {
|
||||
running: <SyncOutlined spin style={{ color: colors.status.running }} />,
|
||||
maintenance: <ClockCircleOutlined style={{ color: colors.status.maintenance }} />,
|
||||
@@ -55,7 +55,7 @@ const DeviceDetailModal = ({
|
||||
return iconMap[status] || <DesktopOutlined style={{ color: colors.text.tertiary }} />;
|
||||
};
|
||||
|
||||
const getDeviceTypeColor = (type) => {
|
||||
const getDeviceTypeColor = type => {
|
||||
return colors.device[type] || colors.device.other;
|
||||
};
|
||||
|
||||
@@ -313,7 +313,15 @@ const DeviceDetailModal = ({
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '20px', position: 'relative', zIndex: 1 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '20px',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="device-icon-wrapper"
|
||||
style={{
|
||||
@@ -328,9 +336,7 @@ const DeviceDetailModal = ({
|
||||
boxShadow: `0 8px 32px rgba(0, 0, 0, 0.2)`,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '32px', color: '#fff' }}>
|
||||
{getDeviceTypeIcon(device.type)}
|
||||
</div>
|
||||
<div style={{ fontSize: '32px', color: '#fff' }}>{getDeviceTypeIcon(device.type)}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
@@ -434,7 +440,7 @@ const DeviceDetailModal = ({
|
||||
paddingTop: '16px',
|
||||
}}
|
||||
>
|
||||
{tabItems.map((tab) => (
|
||||
{tabItems.map(tab => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
@@ -509,19 +515,34 @@ const DeviceDetailModal = ({
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<InfoItem label="功率消耗" value={device.powerConsumption ? `${device.powerConsumption}W` : '-'} />
|
||||
<InfoItem
|
||||
label="功率消耗"
|
||||
value={device.powerConsumption ? `${device.powerConsumption}W` : '-'}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<InfoItem label="设备高度" value={device.height ? `${device.height}U` : '-'} />
|
||||
</Col>
|
||||
</Row>
|
||||
{device.description && (
|
||||
<div style={{ marginTop: '16px', paddingTop: '16px', borderTop: `1px dashed ${colors.border.light}` }}>
|
||||
<div
|
||||
style={{
|
||||
marginTop: '16px',
|
||||
paddingTop: '16px',
|
||||
borderTop: `1px dashed ${colors.border.light}`,
|
||||
}}
|
||||
>
|
||||
<InfoItem label="设备描述" value={device.description} fullWidth />
|
||||
</div>
|
||||
)}
|
||||
{device.customFields && Object.keys(device.customFields).length > 0 && (
|
||||
<div style={{ marginTop: '16px', paddingTop: '16px', borderTop: `1px dashed ${colors.border.light}` }}>
|
||||
<div
|
||||
style={{
|
||||
marginTop: '16px',
|
||||
paddingTop: '16px',
|
||||
borderTop: `1px dashed ${colors.border.light}`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
@@ -536,7 +557,7 @@ const DeviceDetailModal = ({
|
||||
</div>
|
||||
<Row gutter={[16, 16]}>
|
||||
{Object.entries(device.customFields).map(([key, value]) => {
|
||||
const fieldConfig = deviceFields.find((f) => f.fieldName === key);
|
||||
const fieldConfig = deviceFields.find(f => f.fieldName === key);
|
||||
const displayName = fieldConfig?.displayName || key;
|
||||
return (
|
||||
<Col span={8} key={key}>
|
||||
@@ -609,12 +630,23 @@ const DeviceDetailModal = ({
|
||||
>
|
||||
<Row gutter={[24, 16]}>
|
||||
<Col span={8}>
|
||||
<InfoItem label="购买日期" value={device.purchaseDate ? new Date(device.purchaseDate).toLocaleDateString('zh-CN') : '-'} />
|
||||
<InfoItem
|
||||
label="购买日期"
|
||||
value={
|
||||
device.purchaseDate
|
||||
? new Date(device.purchaseDate).toLocaleDateString('zh-CN')
|
||||
: '-'
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<InfoItem
|
||||
label="保修到期"
|
||||
value={device.warrantyExpiry ? new Date(device.warrantyExpiry).toLocaleDateString('zh-CN') : '-'}
|
||||
value={
|
||||
device.warrantyExpiry
|
||||
? new Date(device.warrantyExpiry).toLocaleDateString('zh-CN')
|
||||
: '-'
|
||||
}
|
||||
status={isWarrantyExpired ? 'danger' : undefined}
|
||||
/>
|
||||
</Col>
|
||||
@@ -655,7 +687,9 @@ const DeviceDetailModal = ({
|
||||
gap: '10px',
|
||||
}}
|
||||
>
|
||||
<ExclamationCircleOutlined style={{ color: colors.error.main, fontSize: '18px' }} />
|
||||
<ExclamationCircleOutlined
|
||||
style={{ color: colors.error.main, fontSize: '18px' }}
|
||||
/>
|
||||
<span style={{ color: colors.error.main, fontSize: '13px', fontWeight: 500 }}>
|
||||
设备已过保,建议续保
|
||||
</span>
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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 {
|
||||
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';
|
||||
@@ -48,14 +66,19 @@ const DeviceFormModal = ({
|
||||
}
|
||||
form.setFieldsValue(initialValues);
|
||||
if (editingDevice.rackId) {
|
||||
const rack = racks.find((r) => r.rackId === editingDevice.rackId);
|
||||
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);
|
||||
checkPositionConflict(
|
||||
editingDevice.rackId,
|
||||
editingDevice.position,
|
||||
editingDevice.height,
|
||||
editingDevice.deviceId
|
||||
);
|
||||
}
|
||||
} else {
|
||||
form.resetFields();
|
||||
@@ -95,7 +118,7 @@ const DeviceFormModal = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleRackChange = (value) => {
|
||||
const handleRackChange = value => {
|
||||
setSelectedRackId(value);
|
||||
const position = form.getFieldValue('position');
|
||||
const height = form.getFieldValue('height');
|
||||
@@ -106,7 +129,7 @@ const DeviceFormModal = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handlePositionChange = (value) => {
|
||||
const handlePositionChange = value => {
|
||||
const height = form.getFieldValue('height');
|
||||
if (selectedRackId && value) {
|
||||
checkPositionConflict(selectedRackId, value, height, editingDevice?.deviceId);
|
||||
@@ -115,7 +138,7 @@ const DeviceFormModal = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleHeightChange = (value) => {
|
||||
const handleHeightChange = value => {
|
||||
const position = form.getFieldValue('position');
|
||||
if (selectedRackId && position) {
|
||||
checkPositionConflict(selectedRackId, position, value, editingDevice?.deviceId);
|
||||
@@ -124,7 +147,7 @@ const DeviceFormModal = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (values) => {
|
||||
const handleSubmit = values => {
|
||||
if (positionConflict) {
|
||||
return;
|
||||
}
|
||||
@@ -132,14 +155,14 @@ const DeviceFormModal = ({
|
||||
onSubmit(deviceData);
|
||||
};
|
||||
|
||||
const handleRoomChange = (value) => {
|
||||
const handleRoomChange = value => {
|
||||
setSelectedRoomId(value);
|
||||
setSelectedRackId(null);
|
||||
setPositionConflict(null);
|
||||
form.setFieldValue('rackId', undefined);
|
||||
};
|
||||
|
||||
const renderFieldControl = (field) => {
|
||||
const renderFieldControl = field => {
|
||||
switch (field.fieldType) {
|
||||
case 'number':
|
||||
return (
|
||||
@@ -177,7 +200,7 @@ const DeviceFormModal = ({
|
||||
className="form-input-enhanced"
|
||||
>
|
||||
{Array.isArray(field.options) &&
|
||||
field.options.map((option) => (
|
||||
field.options.map(option => (
|
||||
<Option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</Option>
|
||||
@@ -196,11 +219,15 @@ const DeviceFormModal = ({
|
||||
};
|
||||
|
||||
const filteredFields = deviceFields.filter(
|
||||
(field) => field.fieldName !== 'deviceId' && field.fieldName !== 'rackId' && field.fieldName !== 'position' && field.fieldName !== 'height'
|
||||
field =>
|
||||
field.fieldName !== 'deviceId' &&
|
||||
field.fieldName !== 'rackId' &&
|
||||
field.fieldName !== 'position' &&
|
||||
field.fieldName !== 'height'
|
||||
);
|
||||
|
||||
const formItems = [];
|
||||
filteredFields.forEach((field) => {
|
||||
filteredFields.forEach(field => {
|
||||
if (field.fieldName === 'serialNumber') {
|
||||
formItems.push(
|
||||
<React.Fragment key={field.fieldName}>
|
||||
@@ -210,9 +237,7 @@ const DeviceFormModal = ({
|
||||
label={
|
||||
<span>
|
||||
{field.displayName}
|
||||
{field.required && (
|
||||
<span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>
|
||||
)}
|
||||
{field.required && <span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>}
|
||||
</span>
|
||||
}
|
||||
rules={
|
||||
@@ -266,7 +291,7 @@ const DeviceFormModal = ({
|
||||
optionFilterProp="children"
|
||||
onChange={handleRoomChange}
|
||||
>
|
||||
{rooms.map((room) => (
|
||||
{rooms.map(room => (
|
||||
<Option key={room.roomId} value={room.roomId}>
|
||||
{room.name}
|
||||
</Option>
|
||||
@@ -294,13 +319,14 @@ const DeviceFormModal = ({
|
||||
optionFilterProp="children"
|
||||
onChange={handleRackChange}
|
||||
>
|
||||
{(selectedRoomId ? racks.filter((rack) => rack.roomId === selectedRoomId) : []).map(
|
||||
(rack) => (
|
||||
<Option key={rack.rackId} value={rack.rackId}>
|
||||
{rack.name} ({rack.rackId})
|
||||
</Option>
|
||||
)
|
||||
)}
|
||||
{(selectedRoomId
|
||||
? racks.filter(rack => rack.roomId === selectedRoomId)
|
||||
: []
|
||||
).map(rack => (
|
||||
<Option key={rack.rackId} value={rack.rackId}>
|
||||
{rack.name} ({rack.rackId})
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
@@ -372,9 +398,7 @@ const DeviceFormModal = ({
|
||||
label={
|
||||
<span>
|
||||
{field.displayName}
|
||||
{field.required && (
|
||||
<span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>
|
||||
)}
|
||||
{field.required && <span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>}
|
||||
</span>
|
||||
}
|
||||
rules={
|
||||
@@ -393,9 +417,7 @@ const DeviceFormModal = ({
|
||||
label={
|
||||
<span>
|
||||
{field.displayName}
|
||||
{field.required && (
|
||||
<span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>
|
||||
)}
|
||||
{field.required && <span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>}
|
||||
</span>
|
||||
}
|
||||
rules={
|
||||
|
||||
@@ -39,8 +39,8 @@ const FieldConfigModal = ({
|
||||
);
|
||||
};
|
||||
|
||||
const handleSubmit = async (values) => {
|
||||
const updatedFields = deviceFields.map((field) => ({
|
||||
const handleSubmit = async values => {
|
||||
const updatedFields = deviceFields.map(field => ({
|
||||
fieldId: field.fieldId,
|
||||
fieldName: field.fieldName,
|
||||
displayName: field.displayName,
|
||||
@@ -56,7 +56,7 @@ const FieldConfigModal = ({
|
||||
message.success('字段配置已重置为默认值');
|
||||
};
|
||||
|
||||
const filteredFields = deviceFields.filter((field) => field.fieldName !== 'deviceId');
|
||||
const filteredFields = deviceFields.filter(field => field.fieldName !== 'deviceId');
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -95,7 +95,7 @@ const FieldConfigModal = ({
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredFields.map((field) => (
|
||||
{filteredFields.map(field => (
|
||||
<tr key={field.fieldName} style={{ borderBottom: '1px solid #f0f0f0' }}>
|
||||
<td style={{ padding: '8px' }}>{field.displayName}</td>
|
||||
<td style={{ padding: '8px', textAlign: 'center' }}>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Modal, Upload, Button, Progress, message, Table, Alert, Space, Spin } from 'antd';
|
||||
import { UploadOutlined, DownloadOutlined, CheckCircleOutlined, WarningOutlined, FileTextOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
UploadOutlined,
|
||||
DownloadOutlined,
|
||||
CheckCircleOutlined,
|
||||
WarningOutlined,
|
||||
FileTextOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import { designTokens } from '../../config/theme';
|
||||
|
||||
@@ -12,12 +18,7 @@ const modalHeaderStyle = {
|
||||
fontWeight: 600,
|
||||
};
|
||||
|
||||
const ImportModal = ({
|
||||
visible,
|
||||
deviceFields,
|
||||
onImport,
|
||||
onCancel,
|
||||
}) => {
|
||||
const ImportModal = ({ visible, deviceFields, onImport, onCancel }) => {
|
||||
const [step, setStep] = useState('upload');
|
||||
const [isPreviewing, setIsPreviewing] = useState(false);
|
||||
const [isConfirming, setIsConfirming] = useState(false);
|
||||
@@ -32,7 +33,7 @@ const ImportModal = ({
|
||||
baseURL: '/api',
|
||||
});
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
api.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
@@ -57,7 +58,7 @@ const ImportModal = ({
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const handlePreview = async (file) => {
|
||||
const handlePreview = async file => {
|
||||
const actualFile = file.originFileObj || file;
|
||||
setSelectedFile(actualFile);
|
||||
setPreviewLoading(true);
|
||||
@@ -107,14 +108,14 @@ const ImportModal = ({
|
||||
setImportProgress(progress);
|
||||
setImportPhase(phase);
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
onSuccess: result => {
|
||||
setImportResult(result);
|
||||
setImportProgress(100);
|
||||
setImportPhase('导入完成');
|
||||
setIsConfirming(false);
|
||||
setStep('result');
|
||||
},
|
||||
onError: (error) => {
|
||||
onError: error => {
|
||||
setImportResult({
|
||||
success: false,
|
||||
statistics: {
|
||||
@@ -135,14 +136,14 @@ const ImportModal = ({
|
||||
}
|
||||
};
|
||||
|
||||
const requiredFields = deviceFields.filter((f) => f.visible && f.required);
|
||||
const optionalFields = deviceFields.filter((f) => f.visible && !f.required);
|
||||
const requiredFields = deviceFields.filter(f => f.visible && f.required);
|
||||
const optionalFields = deviceFields.filter(f => f.visible && !f.required);
|
||||
|
||||
const previewColumns = previewData?.fieldList
|
||||
? [
|
||||
...previewData.fieldList
|
||||
.filter((field) => field.fieldName !== 'rackId')
|
||||
.map((field) => ({
|
||||
.filter(field => field.fieldName !== 'rackId')
|
||||
.map(field => ({
|
||||
title: field.displayName + (field.required ? ' *' : ''),
|
||||
dataIndex: field.fieldName,
|
||||
key: field.fieldName,
|
||||
@@ -163,7 +164,7 @@ const ImportModal = ({
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 80 },
|
||||
];
|
||||
|
||||
const getRowClassName = (record) => {
|
||||
const getRowClassName = record => {
|
||||
if (record._hasError) {
|
||||
return 'ant-table-row-error';
|
||||
}
|
||||
@@ -173,9 +174,7 @@ const ImportModal = ({
|
||||
const renderUploadStep = () => (
|
||||
<div>
|
||||
<p style={{ color: '#666', marginBottom: '8px' }}>请上传CSV格式的设备数据文件</p>
|
||||
<p style={{ color: '#999', fontSize: '12px', marginBottom: '20px' }}>
|
||||
支持的编码格式:GBK
|
||||
</p>
|
||||
<p style={{ color: '#999', fontSize: '12px', marginBottom: '20px' }}>支持的编码格式:GBK</p>
|
||||
|
||||
<div
|
||||
style={{
|
||||
@@ -186,15 +185,13 @@ const ImportModal = ({
|
||||
border: '1px solid #f0f0f0',
|
||||
}}
|
||||
>
|
||||
<p style={{ fontWeight: '600', marginBottom: '8px', color: '#333' }}>
|
||||
CSV文件格式要求:
|
||||
</p>
|
||||
<p style={{ fontWeight: '600', marginBottom: '8px', color: '#333' }}>CSV文件格式要求:</p>
|
||||
<div style={{ maxHeight: '200px', overflowY: 'auto' }}>
|
||||
{requiredFields.length > 0 && (
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<span style={{ color: '#d93025', fontWeight: '500' }}>必填字段:</span>
|
||||
<span style={{ color: '#666', fontSize: '13px' }}>
|
||||
{requiredFields.map((f) => f.displayName).join('、')}
|
||||
{requiredFields.map(f => f.displayName).join('、')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -202,7 +199,7 @@ const ImportModal = ({
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<span style={{ color: '#666', fontWeight: '500' }}>可选字段:</span>
|
||||
<span style={{ color: '#666', fontSize: '13px' }}>
|
||||
{optionalFields.map((f) => f.displayName).join('、')}
|
||||
{optionalFields.map(f => f.displayName).join('、')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -290,7 +287,8 @@ const ImportModal = ({
|
||||
{selectedFile?.name || '已选择文件'}
|
||||
</div>
|
||||
<div style={{ color: '#666', fontSize: '13px', marginTop: '2px' }}>
|
||||
共 {previewData?.total || 0} 条记录,已解析 {previewData?.previewCount || 0} 条作为预览
|
||||
共 {previewData?.total || 0} 条记录,已解析 {previewData?.previewCount || 0}{' '}
|
||||
条作为预览
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -384,7 +382,7 @@ const ImportModal = ({
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: false,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
showTotal: total => `共 ${total} 条`,
|
||||
}}
|
||||
scroll={{ x: 'max-content' }}
|
||||
/>
|
||||
@@ -402,7 +400,9 @@ const ImportModal = ({
|
||||
backgroundColor: '#fff7f7',
|
||||
}}
|
||||
>
|
||||
<h4 style={{ color: '#d93025', marginBottom: '12px', fontWeight: '600', fontSize: '13px' }}>
|
||||
<h4
|
||||
style={{ color: '#d93025', marginBottom: '12px', fontWeight: '600', fontSize: '13px' }}
|
||||
>
|
||||
错误详情:
|
||||
</h4>
|
||||
{previewData.errors.slice(0, 10).map((err, index) => (
|
||||
@@ -437,7 +437,8 @@ const ImportModal = ({
|
||||
style={{
|
||||
height: '40px',
|
||||
borderRadius: designTokens.borderRadius.small,
|
||||
background: previewData?.statistics?.invalid > 0 ? '#ccc' : designTokens.colors.primary.gradient,
|
||||
background:
|
||||
previewData?.statistics?.invalid > 0 ? '#ccc' : designTokens.colors.primary.gradient,
|
||||
border: 'none',
|
||||
color: '#ffffff',
|
||||
fontWeight: '500',
|
||||
@@ -703,4 +704,4 @@ const ImportModal = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ImportModal);
|
||||
export default React.memo(ImportModal);
|
||||
|
||||
@@ -27,10 +27,10 @@ const resizableTitleStyles = {
|
||||
},
|
||||
};
|
||||
|
||||
const ResizableTitle = (props) => {
|
||||
const ResizableTitle = props => {
|
||||
const { children, onResize, width, ...restProps } = props;
|
||||
|
||||
const handleMouseDown = (e) => {
|
||||
const handleMouseDown = e => {
|
||||
if (!onResize) return;
|
||||
|
||||
e.preventDefault();
|
||||
@@ -42,7 +42,7 @@ const ResizableTitle = (props) => {
|
||||
const startWidth = th.offsetWidth;
|
||||
const startX = e.clientX;
|
||||
|
||||
const handleMouseMove = (moveEvent) => {
|
||||
const handleMouseMove = moveEvent => {
|
||||
const diff = moveEvent.clientX - startX;
|
||||
const newWidth = Math.max(50, startWidth + diff);
|
||||
onResize(newWidth);
|
||||
|
||||
Reference in New Issue
Block a user