refactor: 统一代码风格并迁移至 ESLint 新配置

style(backend): 格式化模型文件代码
style(frontend): 调整组件代码格式
chore: 删除旧 ESLint 配置并添加新配置
refactor(backend): 重构模型定义语法
style: 统一箭头函数和对象属性简写
This commit is contained in:
zhang1106
2026-03-27 19:12:16 +08:00
parent 6a8d4144ff
commit 63f0cb570e
166 changed files with 15483 additions and 11170 deletions
@@ -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;
+6 -2
View File
@@ -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>
+23 -13
View File
@@ -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}
+12 -9
View File
@@ -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}
+115 -87
View File
@@ -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>
);
+3 -15
View File
@@ -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 X710BCM57414</div>
<div> <strong>制造商</strong>如IntelMellanox</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 X710BCM57414
</div>
<div>
<strong>制造商</strong>选填如IntelMellanox
</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>
)}
+195 -146
View File
@@ -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>
+370 -254
View File
@@ -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/1gigabitethernet1/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/1gigabitethernet1/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);
+21 -14
View File
@@ -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);
+17 -11
View File
@@ -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 = [
{
+5 -11
View File
@@ -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' }}>
+30 -29
View File
@@ -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);