refactor: 统一代码风格并迁移至 ESLint 新配置
style(backend): 格式化模型文件代码 style(frontend): 调整组件代码格式 chore: 删除旧 ESLint 配置并添加新配置 refactor(backend): 重构模型定义语法 style: 统一箭头函数和对象属性简写
This commit is contained in:
@@ -192,7 +192,8 @@ const AppLayout = ({ children }) => {
|
||||
if (path === '/') return 'dashboard';
|
||||
if (path.startsWith('/visualization-3d')) return 'visualization-3d';
|
||||
if (path.startsWith('/rooms') || path.startsWith('/racks')) return 'room-management';
|
||||
if (path.startsWith('/devices') ||
|
||||
if (
|
||||
path.startsWith('/devices') ||
|
||||
path.startsWith('/fields') ||
|
||||
path.startsWith('/cables') ||
|
||||
path.startsWith('/ports') ||
|
||||
@@ -209,7 +210,8 @@ const AppLayout = ({ children }) => {
|
||||
)
|
||||
return 'system-management';
|
||||
if (path.startsWith('/tickets')) return 'ticket-management';
|
||||
if (path.startsWith('/inventory') || path.startsWith('/pending-devices')) return 'inventory-management';
|
||||
if (path.startsWith('/inventory') || path.startsWith('/pending-devices'))
|
||||
return 'inventory-management';
|
||||
return 'dashboard';
|
||||
};
|
||||
|
||||
@@ -652,9 +654,7 @@ const ThemeConfig = () => {
|
||||
</ErrorBoundary>
|
||||
}
|
||||
/>
|
||||
{routeConfig.map(({ path, component: Component }) =>
|
||||
renderRoute(path, Component)
|
||||
)}
|
||||
{routeConfig.map(({ path, component: Component }) => renderRoute(path, Component))}
|
||||
<Route
|
||||
path="/visualization-3d"
|
||||
element={
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -5,17 +5,17 @@
|
||||
|
||||
export const API_CONFIG = {
|
||||
timeout: parseInt(import.meta.env.VITE_API_TIMEOUT, 10) || 30000,
|
||||
|
||||
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
|
||||
|
||||
|
||||
pagination: {
|
||||
defaultPageSize: 10,
|
||||
maxPageSize: 1000,
|
||||
pageSizeOptions: [10, 20, 30, 50, 100],
|
||||
},
|
||||
|
||||
|
||||
debounceDelay: 300,
|
||||
|
||||
|
||||
retry: {
|
||||
maxRetries: 3,
|
||||
retryDelay: 1000,
|
||||
|
||||
@@ -8,7 +8,10 @@ const applyThemeColors = (primaryColor, secondaryColor) => {
|
||||
if (primaryColor) {
|
||||
root.style.setProperty('--primary-color', primaryColor);
|
||||
root.style.setProperty('--primary-light', `${primaryColor}20`);
|
||||
root.style.setProperty('--primary-gradient', `linear-gradient(135deg, ${primaryColor} 0%, ${secondaryColor || '#764ba2'} 100%)`);
|
||||
root.style.setProperty(
|
||||
'--primary-gradient',
|
||||
`linear-gradient(135deg, ${primaryColor} 0%, ${secondaryColor || '#764ba2'} 100%)`
|
||||
);
|
||||
}
|
||||
if (secondaryColor) {
|
||||
root.style.setProperty('--secondary-color', secondaryColor);
|
||||
|
||||
@@ -34,7 +34,7 @@ export const useDangerousOperation = () => {
|
||||
const entityLabel = ENTITY_LABELS[entityType] || entityType;
|
||||
const operationLabel = OPERATION_LABELS[operationType] || operationType;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
const modalKey = `dangerous-${Date.now()}`;
|
||||
|
||||
const handleOk = () => {
|
||||
@@ -74,14 +74,21 @@ export const useDangerousOperation = () => {
|
||||
}
|
||||
description={
|
||||
<Paragraph style={{ marginBottom: 0 }}>
|
||||
{description || `您即将${operationLabel} ${itemCount > 1 ? `${itemCount} 个` : '1 个'}${entityLabel}。`}
|
||||
{description ||
|
||||
`您即将${operationLabel} ${itemCount > 1 ? `${itemCount} 个` : '1 个'}${entityLabel}。`}
|
||||
<br />
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
此操作不可逆,一旦删除将无法恢复。
|
||||
</Text>
|
||||
</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,
|
||||
@@ -106,7 +113,11 @@ export const useDangerousOperation = () => {
|
||||
<Alert
|
||||
message={
|
||||
<Text>
|
||||
为确认此操作,请输入 <Text code strong>CONFIRM</Text>:
|
||||
为确认此操作,请输入{' '}
|
||||
<Text code strong>
|
||||
CONFIRM
|
||||
</Text>
|
||||
:
|
||||
</Text>
|
||||
}
|
||||
type="error"
|
||||
@@ -115,7 +126,7 @@ export const useDangerousOperation = () => {
|
||||
<Input
|
||||
id={`keyword-input-${modalKey}`}
|
||||
placeholder="请输入 CONFIRM"
|
||||
onChange={(e) => {
|
||||
onChange={e => {
|
||||
const inputValue = e.target.value;
|
||||
const okButton = document.querySelector('.ant-modal-confirm .ant-btn-primary');
|
||||
if (okButton) {
|
||||
@@ -212,7 +223,7 @@ export const useDangerousOperation = () => {
|
||||
return { confirm, logOperation };
|
||||
};
|
||||
|
||||
export const confirmDangerousOperation = async (options) => {
|
||||
export const confirmDangerousOperation = async options => {
|
||||
const hook = useDangerousOperation();
|
||||
return hook.confirm(options);
|
||||
};
|
||||
|
||||
@@ -119,15 +119,7 @@ const useIdleTimeout = ({
|
||||
}
|
||||
|
||||
// 定义需要监听的事件
|
||||
const events = [
|
||||
'mousedown',
|
||||
'mousemove',
|
||||
'keydown',
|
||||
'scroll',
|
||||
'touchstart',
|
||||
'click',
|
||||
'wheel',
|
||||
];
|
||||
const events = ['mousedown', 'mousemove', 'keydown', 'scroll', 'touchstart', 'click', 'wheel'];
|
||||
|
||||
// 事件处理函数
|
||||
const handleActivity = () => {
|
||||
|
||||
@@ -16,7 +16,7 @@ export const SCREEN_SIZES = {
|
||||
xl: 'xl',
|
||||
};
|
||||
|
||||
const getScreenSize = (width) => {
|
||||
const getScreenSize = width => {
|
||||
if (width >= BREAKPOINTS.xl) return SCREEN_SIZES.xl;
|
||||
if (width >= BREAKPOINTS.lg) return SCREEN_SIZES.lg;
|
||||
if (width >= BREAKPOINTS.md) return SCREEN_SIZES.md;
|
||||
@@ -24,7 +24,7 @@ const getScreenSize = (width) => {
|
||||
return SCREEN_SIZES.xs;
|
||||
};
|
||||
|
||||
const getScreenSizeConfig = (screenSize) => {
|
||||
const getScreenSizeConfig = screenSize => {
|
||||
const configs = {
|
||||
xs: {
|
||||
showFullButtonLabels: false,
|
||||
@@ -100,4 +100,4 @@ export const useResponsiveLayout = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export default useResponsiveLayout;
|
||||
export default useResponsiveLayout;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const extractNumberFromString = (str) => {
|
||||
const extractNumberFromString = str => {
|
||||
if (!str) return 0;
|
||||
const match = str.match(/\d+/);
|
||||
return match ? parseInt(match[0], 10) : 0;
|
||||
@@ -13,7 +13,7 @@ const naturalSort = (a, b) => {
|
||||
return String(a).localeCompare(String(b), 'zh-CN');
|
||||
};
|
||||
|
||||
const sortRooms = (rooms) => {
|
||||
const sortRooms = rooms => {
|
||||
return [...rooms].sort((a, b) => {
|
||||
const sortOrderA = a.sortOrder ?? a.sort_order ?? 0;
|
||||
const sortOrderB = b.sortOrder ?? b.sort_order ?? 0;
|
||||
@@ -24,7 +24,7 @@ const sortRooms = (rooms) => {
|
||||
});
|
||||
};
|
||||
|
||||
const sortRacksInRoom = (racks) => {
|
||||
const sortRacksInRoom = racks => {
|
||||
return [...racks].sort((a, b) => {
|
||||
const sortOrderA = a.sortOrder ?? a.sort_order ?? 0;
|
||||
const sortOrderB = b.sortOrder ?? b.sort_order ?? 0;
|
||||
@@ -38,7 +38,7 @@ const sortRacksInRoom = (racks) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useSortedRacks = (racks) => {
|
||||
export const useSortedRacks = racks => {
|
||||
return useMemo(() => {
|
||||
if (!racks || !Array.isArray(racks) || racks.length === 0) {
|
||||
return [];
|
||||
@@ -46,7 +46,7 @@ export const useSortedRacks = (racks) => {
|
||||
|
||||
const roomMap = new Map();
|
||||
|
||||
racks.forEach((rack) => {
|
||||
racks.forEach(rack => {
|
||||
if (!rack || !rack.Room) return;
|
||||
|
||||
const roomKey = rack.Room.roomId || rack.Room.id || rack.Room.name;
|
||||
@@ -65,10 +65,13 @@ export const useSortedRacks = (racks) => {
|
||||
|
||||
const sortedRooms = sortRooms(Array.from(roomMap.values()));
|
||||
|
||||
sortedRooms.forEach((room) => {
|
||||
sortedRooms.forEach(room => {
|
||||
room.racks = sortRacksInRoom(room.racks);
|
||||
room.rackCount = room.racks.length;
|
||||
room.totalDevices = room.racks.reduce((sum, r) => sum + (r._count?.Devices || r.deviceCount || 0), 0);
|
||||
room.totalDevices = room.racks.reduce(
|
||||
(sum, r) => sum + (r._count?.Devices || r.deviceCount || 0),
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
return sortedRooms;
|
||||
@@ -83,11 +86,11 @@ export const filterRoomsBySearch = (rooms, searchText) => {
|
||||
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);
|
||||
|
||||
const filteredRacks = room.racks.filter((rack) => {
|
||||
const filteredRacks = room.racks.filter(rack => {
|
||||
const rackNameMatch = rack.name?.toLowerCase().includes(lowerSearch);
|
||||
const rackIdMatch = rack.rackId?.toLowerCase().includes(lowerSearch);
|
||||
return roomNameMatch || roomIdMatch || rackNameMatch || rackIdMatch;
|
||||
@@ -103,7 +106,7 @@ export const filterRoomsBySearch = (rooms, searchText) => {
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((room) => room !== null);
|
||||
.filter(room => room !== null);
|
||||
};
|
||||
|
||||
export const getRackStats = (rack, devices = []) => {
|
||||
@@ -123,4 +126,4 @@ export const getRackStats = (rack, devices = []) => {
|
||||
};
|
||||
};
|
||||
|
||||
export default useSortedRacks;
|
||||
export default useSortedRacks;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -209,12 +209,21 @@ function CategoryManagement() {
|
||||
allowClear
|
||||
prefix={<SearchOutlined />}
|
||||
/>
|
||||
<Select value={status} onChange={setStatus} style={{ ...selectStyles.base, width: 120 }}>
|
||||
<Select
|
||||
value={status}
|
||||
onChange={setStatus}
|
||||
style={{ ...selectStyles.base, width: 120 }}
|
||||
>
|
||||
<Option value="all">所有状态</Option>
|
||||
<Option value="active">启用</Option>
|
||||
<Option value="inactive">停用</Option>
|
||||
</Select>
|
||||
<Button onClick={() => fetchCategories()} style={{ height: '40px', borderRadius: '10px' }}>刷新</Button>
|
||||
<Button
|
||||
onClick={() => fetchCategories()}
|
||||
style={{ height: '40px', borderRadius: '10px' }}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
@@ -249,12 +258,22 @@ function CategoryManagement() {
|
||||
<Input placeholder="请输入分类名称" style={inputStyles.form} />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea rows={3} placeholder={inputPlaceholders.description} maxLength={200} showCount style={textAreaStyles.base} />
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
placeholder={inputPlaceholders.description}
|
||||
maxLength={200}
|
||||
showCount
|
||||
style={textAreaStyles.base}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="sortOrder" label="排序">
|
||||
<InputNumber min={0} placeholder="数值越小越靠前" style={inputNumberStyles.base} />
|
||||
</Form.Item>
|
||||
<Form.Item name="status" label="状态" rules={[inputValidationRules.required('请选择状态')]}>
|
||||
<Form.Item
|
||||
name="status"
|
||||
label="状态"
|
||||
rules={[inputValidationRules.required('请选择状态')]}
|
||||
>
|
||||
<Select style={selectStyles.base}>
|
||||
<Option value="active">启用</Option>
|
||||
<Option value="inactive">停用</Option>
|
||||
@@ -262,10 +281,16 @@ function CategoryManagement() {
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit" style={{ height: '40px', borderRadius: '10px' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
style={{ height: '40px', borderRadius: '10px' }}
|
||||
>
|
||||
{editingCategory ? '更新' : '创建'}
|
||||
</Button>
|
||||
<Button onClick={handleCancel} style={{ height: '40px', borderRadius: '10px' }}>取消</Button>
|
||||
<Button onClick={handleCancel} style={{ height: '40px', borderRadius: '10px' }}>
|
||||
取消
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
@@ -84,7 +84,11 @@ function ConsumableLogs() {
|
||||
setLoading(true);
|
||||
const params = { page, pageSize };
|
||||
|
||||
if (currentFilters.operationType && currentFilters.operationType !== 'all' && currentFilters.operationType.length > 0) {
|
||||
if (
|
||||
currentFilters.operationType &&
|
||||
currentFilters.operationType !== 'all' &&
|
||||
currentFilters.operationType.length > 0
|
||||
) {
|
||||
params.operationType = currentFilters.operationType.join(',');
|
||||
}
|
||||
if (currentFilters.consumableId) {
|
||||
@@ -169,7 +173,9 @@ function ConsumableLogs() {
|
||||
<Space direction="vertical" size={0}>
|
||||
<span>{value}</span>
|
||||
{record.isConsumableDeleted && (
|
||||
<Tag color="red" size="small">已删除</Tag>
|
||||
<Tag color="red" size="small">
|
||||
已删除
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
</Tooltip>
|
||||
@@ -288,13 +294,15 @@ function ConsumableLogs() {
|
||||
width: 200,
|
||||
render: value => (
|
||||
<Tooltip title={value || '-'}>
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
maxWidth: '180px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
maxWidth: '180px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{value || '-'}
|
||||
</span>
|
||||
</Tooltip>
|
||||
@@ -648,7 +656,11 @@ function ConsumableLogs() {
|
||||
导出 <DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<Button icon={<UploadOutlined />} onClick={() => setImportModalVisible(true)} style={{ height: '40px', borderRadius: '10px' }}>
|
||||
<Button
|
||||
icon={<UploadOutlined />}
|
||||
onClick={() => setImportModalVisible(true)}
|
||||
style={{ height: '40px', borderRadius: '10px' }}
|
||||
>
|
||||
导入
|
||||
</Button>
|
||||
</Space>
|
||||
@@ -735,17 +747,29 @@ function ConsumableLogs() {
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleEditSubmit}>
|
||||
<Form.Item label="操作原因" name="reason">
|
||||
<Input.TextArea rows={2} placeholder={inputPlaceholders.reason} style={textAreaStyles.base} />
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
placeholder={inputPlaceholders.reason}
|
||||
style={textAreaStyles.base}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="备注" name="notes">
|
||||
<Input.TextArea rows={3} placeholder={inputPlaceholders.notes} style={textAreaStyles.base} />
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
placeholder={inputPlaceholders.notes}
|
||||
style={textAreaStyles.base}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="修改原因"
|
||||
name="modificationReason"
|
||||
rules={[inputValidationRules.required('请输入修改原因')]}
|
||||
>
|
||||
<Input.TextArea rows={2} placeholder="请输入修改原因(必填)" style={textAreaStyles.base} />
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
placeholder="请输入修改原因(必填)"
|
||||
style={textAreaStyles.base}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="修改人" name="operator">
|
||||
<Input placeholder={inputPlaceholders.operator} style={inputStyles.form} />
|
||||
@@ -788,7 +812,11 @@ function ConsumableLogs() {
|
||||
<div style={{ fontSize: 12, color: '#666' }}>
|
||||
<p>
|
||||
<strong>耗材:</strong> {item.consumableName} ({item.consumableId})
|
||||
{item.isConsumableDeleted && <Tag color="red" style={{ marginLeft: 8 }}>已删除</Tag>}
|
||||
{item.isConsumableDeleted && (
|
||||
<Tag color="red" style={{ marginLeft: 8 }}>
|
||||
已删除
|
||||
</Tag>
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<strong>操作人:</strong> {item.operator}
|
||||
@@ -806,8 +834,8 @@ function ConsumableLogs() {
|
||||
{item.consumableSnapshot && (
|
||||
<p>
|
||||
<strong>快照信息:</strong> 分类:{item.consumableSnapshot.category || '-'} |
|
||||
单位:{item.consumableSnapshot.unit || '-'} |
|
||||
单价:{item.consumableSnapshot.unitPrice || '-'}
|
||||
单位:{item.consumableSnapshot.unit || '-'} | 单价:
|
||||
{item.consumableSnapshot.unitPrice || '-'}
|
||||
</p>
|
||||
)}
|
||||
{item.modifiedBy && (
|
||||
@@ -854,12 +882,25 @@ function ConsumableLogs() {
|
||||
) : (
|
||||
<div>
|
||||
<Card size="small" title="基本信息" style={{ marginBottom: 16 }}>
|
||||
<p><strong>归档ID:</strong> <code>{currentArchive.archiveId}</code></p>
|
||||
<p><strong>耗材ID:</strong> <code>{currentArchive.consumableId}</code></p>
|
||||
<p><strong>耗材名称:</strong> {currentArchive.consumableName}</p>
|
||||
<p><strong>删除人:</strong> {currentArchive.deletedBy}</p>
|
||||
<p><strong>删除时间:</strong> {dayjs(currentArchive.deletedAt).format('YYYY-MM-DD HH:mm:ss')}</p>
|
||||
<p><strong>删除原因:</strong> {currentArchive.deleteReason || '-'}</p>
|
||||
<p>
|
||||
<strong>归档ID:</strong> <code>{currentArchive.archiveId}</code>
|
||||
</p>
|
||||
<p>
|
||||
<strong>耗材ID:</strong> <code>{currentArchive.consumableId}</code>
|
||||
</p>
|
||||
<p>
|
||||
<strong>耗材名称:</strong> {currentArchive.consumableName}
|
||||
</p>
|
||||
<p>
|
||||
<strong>删除人:</strong> {currentArchive.deletedBy}
|
||||
</p>
|
||||
<p>
|
||||
<strong>删除时间:</strong>{' '}
|
||||
{dayjs(currentArchive.deletedAt).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</p>
|
||||
<p>
|
||||
<strong>删除原因:</strong> {currentArchive.deleteReason || '-'}
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card size="small" title="操作统计" style={{ marginBottom: 16 }}>
|
||||
@@ -875,20 +916,46 @@ function ConsumableLogs() {
|
||||
</Col>
|
||||
</Row>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<p><strong>首次操作:</strong> {currentArchive.firstOperationAt ? dayjs(currentArchive.firstOperationAt).format('YYYY-MM-DD HH:mm:ss') : '-'}</p>
|
||||
<p><strong>最后操作:</strong> {currentArchive.lastOperationAt ? dayjs(currentArchive.lastOperationAt).format('YYYY-MM-DD HH:mm:ss') : '-'}</p>
|
||||
<p><strong>删除时库存:</strong> {currentArchive.finalStock}</p>
|
||||
<p>
|
||||
<strong>首次操作:</strong>{' '}
|
||||
{currentArchive.firstOperationAt
|
||||
? dayjs(currentArchive.firstOperationAt).format('YYYY-MM-DD HH:mm:ss')
|
||||
: '-'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>最后操作:</strong>{' '}
|
||||
{currentArchive.lastOperationAt
|
||||
? dayjs(currentArchive.lastOperationAt).format('YYYY-MM-DD HH:mm:ss')
|
||||
: '-'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>删除时库存:</strong> {currentArchive.finalStock}
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
{currentArchive.consumableSnapshot && (
|
||||
<Card size="small" title="耗材快照">
|
||||
<p><strong>分类:</strong> {currentArchive.consumableSnapshot.category || '-'}</p>
|
||||
<p><strong>单位:</strong> {currentArchive.consumableSnapshot.unit || '-'}</p>
|
||||
<p><strong>单价:</strong> {currentArchive.consumableSnapshot.unitPrice || '-'}</p>
|
||||
<p><strong>供应商:</strong> {currentArchive.consumableSnapshot.supplier || '-'}</p>
|
||||
<p><strong>位置:</strong> {currentArchive.consumableSnapshot.location || '-'}</p>
|
||||
<p><strong>最小库存:</strong> {currentArchive.consumableSnapshot.minStock || '-'}</p>
|
||||
<p><strong>最大库存:</strong> {currentArchive.consumableSnapshot.maxStock || '-'}</p>
|
||||
<p>
|
||||
<strong>分类:</strong> {currentArchive.consumableSnapshot.category || '-'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>单位:</strong> {currentArchive.consumableSnapshot.unit || '-'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>单价:</strong> {currentArchive.consumableSnapshot.unitPrice || '-'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>供应商:</strong> {currentArchive.consumableSnapshot.supplier || '-'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>位置:</strong> {currentArchive.consumableSnapshot.location || '-'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>最小库存:</strong> {currentArchive.consumableSnapshot.minStock || '-'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>最大库存:</strong> {currentArchive.consumableSnapshot.maxStock || '-'}
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -49,10 +49,7 @@ import dayjs from 'dayjs';
|
||||
import api from '../api';
|
||||
import { consumableRecordAPI, consumableCategoryAPI, consumableAPI } from '../api/cache';
|
||||
import { message } from 'antd';
|
||||
import {
|
||||
selectStyles,
|
||||
datePickerStyles,
|
||||
} from '../styles/deviceManagementStyles';
|
||||
import { selectStyles, datePickerStyles } from '../styles/deviceManagementStyles';
|
||||
import { designTokens } from '../config/theme';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
@@ -145,14 +142,16 @@ const QuickFilterBtn = styled(Button)`
|
||||
padding: 0 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border: 1px solid ${props => props.$active ? designTokens.colors.primary.main : designTokens.colors.border};
|
||||
background: ${props => props.$active ? designTokens.colors.primary.main : 'transparent'};
|
||||
color: ${props => props.$active ? 'white' : designTokens.colors.text.secondary};
|
||||
|
||||
border: 1px solid
|
||||
${props => (props.$active ? designTokens.colors.primary.main : designTokens.colors.border)};
|
||||
background: ${props => (props.$active ? designTokens.colors.primary.main : 'transparent')};
|
||||
color: ${props => (props.$active ? 'white' : designTokens.colors.text.secondary)};
|
||||
|
||||
&:hover {
|
||||
border-color: ${designTokens.colors.primary.main};
|
||||
color: ${props => props.$active ? 'white' : designTokens.colors.primary.main};
|
||||
background: ${props => props.$active ? designTokens.colors.primary.main : 'rgba(99, 102, 241, 0.05)'};
|
||||
color: ${props => (props.$active ? 'white' : designTokens.colors.primary.main)};
|
||||
background: ${props =>
|
||||
props.$active ? designTokens.colors.primary.main : 'rgba(99, 102, 241, 0.05)'};
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -304,17 +303,17 @@ const StatsCard = styled(motion.div)`
|
||||
font-weight: 600;
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
|
||||
|
||||
&.up {
|
||||
color: ${designTokens.colors.success.main};
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
|
||||
|
||||
&.down {
|
||||
color: ${designTokens.colors.error.main};
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
|
||||
&.neutral {
|
||||
color: ${designTokens.colors.text.secondary};
|
||||
background: rgba(107, 114, 128, 0.1);
|
||||
@@ -478,7 +477,7 @@ const ComparisonItem = styled.div`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
|
||||
|
||||
strong {
|
||||
color: ${props => props.$color};
|
||||
font-weight: 600;
|
||||
@@ -519,7 +518,7 @@ const CategoryCard = styled(motion.div)`
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px ${props => props.$color}20;
|
||||
border-color: ${props => props.$color}40;
|
||||
|
||||
|
||||
&::before {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -567,7 +566,7 @@ const CategoryCard = styled(motion.div)`
|
||||
color: ${designTokens.colors.text.secondary};
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid ${designTokens.colors.border}40;
|
||||
|
||||
|
||||
strong {
|
||||
color: ${designTokens.colors.text.primary};
|
||||
font-weight: 600;
|
||||
@@ -731,7 +730,7 @@ const ConsumableStatistics = () => {
|
||||
const statsResponse = await consumableRecordAPI.statistics(params);
|
||||
console.log('[统计] 返回:', statsResponse);
|
||||
console.log('[统计] 最近记录:', statsResponse?.recentRecords);
|
||||
|
||||
|
||||
setStats({
|
||||
inCount: statsResponse?.inCount || 0,
|
||||
outCount: statsResponse?.outCount || 0,
|
||||
@@ -742,7 +741,7 @@ const ConsumableStatistics = () => {
|
||||
|
||||
const summaryResponse = await consumableAPI.getStatistics();
|
||||
console.log('[汇总] 返回:', summaryResponse);
|
||||
|
||||
|
||||
setSummary({
|
||||
total: summaryResponse?.total || 0,
|
||||
lowStock: summaryResponse?.lowStock || 0,
|
||||
@@ -808,7 +807,7 @@ const ConsumableStatistics = () => {
|
||||
};
|
||||
}, [realTimeRefresh]);
|
||||
|
||||
const handleQuickFilter = (key) => {
|
||||
const handleQuickFilter = key => {
|
||||
setQuickFilter(key);
|
||||
const filter = quickFilters.find(f => f.key === key);
|
||||
if (filter) {
|
||||
@@ -823,10 +822,7 @@ const ConsumableStatistics = () => {
|
||||
const handleRefresh = () => {
|
||||
setLoading(true);
|
||||
setIsAutoRefreshing(false);
|
||||
Promise.all([
|
||||
loadStatistics(false),
|
||||
loadLowStockItems(false)
|
||||
]).finally(() => {
|
||||
Promise.all([loadStatistics(false), loadLowStockItems(false)]).finally(() => {
|
||||
setLoading(false);
|
||||
if (!realTimeRefresh) {
|
||||
message.success('数据已手动刷新');
|
||||
@@ -838,22 +834,30 @@ const ConsumableStatistics = () => {
|
||||
message.info('导出功能开发中...');
|
||||
};
|
||||
|
||||
const getCategoryColor = (category) => {
|
||||
const getCategoryColor = category => {
|
||||
const predefinedColors = [
|
||||
'#6366f1', '#10b981', '#f59e0b', '#ec4899', '#8b5cf6',
|
||||
'#06b6d4', '#f97316', '#14b8a6', '#ef4444', '#3b82f6'
|
||||
'#6366f1',
|
||||
'#10b981',
|
||||
'#f59e0b',
|
||||
'#ec4899',
|
||||
'#8b5cf6',
|
||||
'#06b6d4',
|
||||
'#f97316',
|
||||
'#14b8a6',
|
||||
'#ef4444',
|
||||
'#3b82f6',
|
||||
];
|
||||
|
||||
|
||||
const colorMap = {
|
||||
'网络设备': '#6366f1',
|
||||
'线缆': '#10b981',
|
||||
'配件': '#f59e0b',
|
||||
'工具': '#ec4899',
|
||||
'其他': '#6b7280',
|
||||
网络设备: '#6366f1',
|
||||
线缆: '#10b981',
|
||||
配件: '#f59e0b',
|
||||
工具: '#ec4899',
|
||||
其他: '#6b7280',
|
||||
};
|
||||
|
||||
|
||||
if (colorMap[category]) return colorMap[category];
|
||||
|
||||
|
||||
let hash = 0;
|
||||
for (let i = 0; i < category.length; i++) {
|
||||
hash = category.charCodeAt(i) + ((hash << 5) - hash);
|
||||
@@ -880,23 +884,27 @@ const ConsumableStatistics = () => {
|
||||
<WarningOutlined />
|
||||
</Avatar>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{
|
||||
fontWeight: 600,
|
||||
color: designTokens.colors.text.primary,
|
||||
fontSize: '13px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
color: designTokens.colors.text.primary,
|
||||
fontSize: '13px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
color: designTokens.colors.text.secondary,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: designTokens.colors.text.secondary,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{record.specification || record.category || '-'}
|
||||
</div>
|
||||
</div>
|
||||
@@ -913,12 +921,16 @@ const ConsumableStatistics = () => {
|
||||
const min = record.minStock || 0;
|
||||
const isLow = current < min;
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'center', gap: 4 }}>
|
||||
<span style={{
|
||||
fontWeight: 700,
|
||||
fontSize: '14px',
|
||||
color: isLow ? designTokens.colors.error.main : designTokens.colors.text.primary,
|
||||
}}>
|
||||
<div
|
||||
style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'center', gap: 4 }}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 700,
|
||||
fontSize: '14px',
|
||||
color: isLow ? designTokens.colors.error.main : designTokens.colors.text.primary,
|
||||
}}
|
||||
>
|
||||
{current}
|
||||
</span>
|
||||
<span style={{ color: designTokens.colors.text.secondary, fontSize: '11px' }}>/</span>
|
||||
@@ -942,13 +954,20 @@ const ConsumableStatistics = () => {
|
||||
const currentStock = record.currentStock || 0;
|
||||
|
||||
if (minStock <= 0) {
|
||||
return <Text type="secondary" style={{ fontSize: '11px' }}>未设置</Text>;
|
||||
return (
|
||||
<Text type="secondary" style={{ fontSize: '11px' }}>
|
||||
未设置
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const rate = Math.min(100, Math.round((currentStock / minStock) * 100));
|
||||
const color = rate < 30 ? designTokens.colors.error.main :
|
||||
rate < 60 ? designTokens.colors.warning.main :
|
||||
designTokens.colors.success.main;
|
||||
const color =
|
||||
rate < 30
|
||||
? designTokens.colors.error.main
|
||||
: rate < 60
|
||||
? designTokens.colors.warning.main
|
||||
: designTokens.colors.success.main;
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8 }}>
|
||||
@@ -959,13 +978,15 @@ const ConsumableStatistics = () => {
|
||||
showInfo={false}
|
||||
style={{ width: 60 }}
|
||||
/>
|
||||
<span style={{
|
||||
fontWeight: 600,
|
||||
fontSize: '12px',
|
||||
color,
|
||||
minWidth: 32,
|
||||
textAlign: 'right',
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: '12px',
|
||||
color,
|
||||
minWidth: 32,
|
||||
textAlign: 'right',
|
||||
}}
|
||||
>
|
||||
{rate}%
|
||||
</span>
|
||||
</div>
|
||||
@@ -980,7 +1001,7 @@ const ConsumableStatistics = () => {
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 80,
|
||||
render: (type) => (
|
||||
render: type => (
|
||||
<Tag
|
||||
icon={type === 'in' ? <ArrowDownOutlined /> : <ArrowUpOutlined />}
|
||||
color={type === 'in' ? 'success' : 'processing'}
|
||||
@@ -1000,14 +1021,18 @@ const ConsumableStatistics = () => {
|
||||
<Avatar
|
||||
size={30}
|
||||
style={{
|
||||
background: record.category ? getCategoryColor(record.category) : designTokens.colors.info.main,
|
||||
background: record.category
|
||||
? getCategoryColor(record.category)
|
||||
: designTokens.colors.info.main,
|
||||
fontSize: '12px',
|
||||
}}
|
||||
>
|
||||
{record.category?.charAt(0) || '耗'}
|
||||
</Avatar>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, fontSize: '14px', color: designTokens.colors.text.primary }}>
|
||||
<div
|
||||
style={{ fontWeight: 600, fontSize: '14px', color: designTokens.colors.text.primary }}
|
||||
>
|
||||
{text || '-'}
|
||||
</div>
|
||||
{record.category && (
|
||||
@@ -1030,10 +1055,14 @@ const ConsumableStatistics = () => {
|
||||
strong
|
||||
style={{
|
||||
fontSize: '15px',
|
||||
color: record.type === 'in' ? designTokens.colors.success.main : designTokens.colors.error.main,
|
||||
color:
|
||||
record.type === 'in'
|
||||
? designTokens.colors.success.main
|
||||
: designTokens.colors.error.main,
|
||||
}}
|
||||
>
|
||||
{record.type === 'in' ? '+' : '-'}{quantity} {record.unit || '个'}
|
||||
{record.type === 'in' ? '+' : '-'}
|
||||
{quantity} {record.unit || '个'}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
@@ -1042,7 +1071,7 @@ const ConsumableStatistics = () => {
|
||||
dataIndex: 'operator',
|
||||
key: 'operator',
|
||||
width: 100,
|
||||
render: (operator) => (
|
||||
render: operator => (
|
||||
<Space size={6}>
|
||||
<Avatar size={24} style={{ background: designTokens.colors.info.main, fontSize: '12px' }}>
|
||||
{operator?.charAt(0) || '?'}
|
||||
@@ -1056,7 +1085,7 @@ const ConsumableStatistics = () => {
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 140,
|
||||
render: (date) => (
|
||||
render: date => (
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
{dayjs(date).format('MM-DD HH:mm')}
|
||||
</Text>
|
||||
@@ -1086,13 +1115,19 @@ const ConsumableStatistics = () => {
|
||||
</TitleSection>
|
||||
<Space>
|
||||
{lastUpdateTime && (
|
||||
<div style={{ fontSize: 12, color: designTokens.colors.text.secondary, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
{isAutoRefreshing ? (
|
||||
<Spin size="small" />
|
||||
) : (
|
||||
<span style={{ fontSize: 10 }}>●</span>
|
||||
)}
|
||||
{isAutoRefreshing ? '刷新中...' : `更新于 ${dayjs(lastUpdateTime).format('HH:mm:ss')}`}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: designTokens.colors.text.secondary,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
{isAutoRefreshing ? <Spin size="small" /> : <span style={{ fontSize: 10 }}>●</span>}
|
||||
{isAutoRefreshing
|
||||
? '刷新中...'
|
||||
: `更新于 ${dayjs(lastUpdateTime).format('HH:mm:ss')}`}
|
||||
</div>
|
||||
)}
|
||||
<Tooltip title={realTimeRefresh ? '已开启30秒自动刷新' : '已关闭自动刷新'}>
|
||||
@@ -1154,7 +1189,7 @@ const ConsumableStatistics = () => {
|
||||
<span className="filter-label">时间范围</span>
|
||||
<RangePicker
|
||||
value={dateRange}
|
||||
onChange={(dates) => {
|
||||
onChange={dates => {
|
||||
setDateRange(dates);
|
||||
setQuickFilter(null);
|
||||
}}
|
||||
@@ -1174,7 +1209,9 @@ const ConsumableStatistics = () => {
|
||||
>
|
||||
<Option value="all">全部类别</Option>
|
||||
{categories.map(cat => (
|
||||
<Option key={cat.id} value={cat.name}>{cat.name}</Option>
|
||||
<Option key={cat.id} value={cat.name}>
|
||||
{cat.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</FilterItem>
|
||||
@@ -1227,7 +1264,9 @@ const ConsumableStatistics = () => {
|
||||
>
|
||||
<div className="card-content">
|
||||
<div className="card-header">
|
||||
<div className="card-icon"><DatabaseOutlined /></div>
|
||||
<div className="card-icon">
|
||||
<DatabaseOutlined />
|
||||
</div>
|
||||
<div className="card-trend neutral">
|
||||
<AppstoreOutlined /> 总览
|
||||
</div>
|
||||
@@ -1250,7 +1289,9 @@ const ConsumableStatistics = () => {
|
||||
>
|
||||
<div className="card-content">
|
||||
<div className="card-header">
|
||||
<div className="card-icon"><WarningOutlined /></div>
|
||||
<div className="card-icon">
|
||||
<WarningOutlined />
|
||||
</div>
|
||||
{summary?.lowStock > 0 && (
|
||||
<div className="card-trend down">
|
||||
<ExclamationCircleOutlined /> 需关注
|
||||
@@ -1277,7 +1318,9 @@ const ConsumableStatistics = () => {
|
||||
>
|
||||
<div className="card-content">
|
||||
<div className="card-header">
|
||||
<div className="card-icon"><ArrowDownOutlined /></div>
|
||||
<div className="card-icon">
|
||||
<ArrowDownOutlined />
|
||||
</div>
|
||||
<div className="card-trend up">
|
||||
<RiseOutlined /> 入库
|
||||
</div>
|
||||
@@ -1302,7 +1345,9 @@ const ConsumableStatistics = () => {
|
||||
>
|
||||
<div className="card-content">
|
||||
<div className="card-header">
|
||||
<div className="card-icon"><ArrowUpOutlined /></div>
|
||||
<div className="card-icon">
|
||||
<ArrowUpOutlined />
|
||||
</div>
|
||||
<div className="card-trend neutral">
|
||||
<FallOutlined /> 出库
|
||||
</div>
|
||||
@@ -1318,10 +1363,16 @@ const ConsumableStatistics = () => {
|
||||
</StatsGrid>
|
||||
|
||||
<BentoGrid variants={containerVariants} initial="hidden" animate="visible">
|
||||
<BentoCard variants={itemVariants} $col="span 6" $iconBg={designTokens.colors.info.gradient}>
|
||||
<BentoCard
|
||||
variants={itemVariants}
|
||||
$col="span 6"
|
||||
$iconBg={designTokens.colors.info.gradient}
|
||||
>
|
||||
<div className="card-header">
|
||||
<div className="header-left">
|
||||
<div className="header-icon"><ShoppingCartOutlined /></div>
|
||||
<div className="header-icon">
|
||||
<ShoppingCartOutlined />
|
||||
</div>
|
||||
<span className="header-title">出入库统计</span>
|
||||
</div>
|
||||
<div className="header-extra">
|
||||
@@ -1334,28 +1385,36 @@ const ConsumableStatistics = () => {
|
||||
$bg="rgba(16, 185, 129, 0.04)"
|
||||
$color={designTokens.colors.success.main}
|
||||
>
|
||||
<div className="item-icon"><ArrowDownOutlined /></div>
|
||||
<div className="item-icon">
|
||||
<ArrowDownOutlined />
|
||||
</div>
|
||||
<div className="item-value">{stats?.inCount || 0}</div>
|
||||
<div className="item-label">入库次数</div>
|
||||
<div className="item-sub">共 <strong>{stats?.inQuantity || 0}</strong> 件</div>
|
||||
<div className="item-sub">
|
||||
共 <strong>{stats?.inQuantity || 0}</strong> 件
|
||||
</div>
|
||||
</ComparisonItem>
|
||||
<ComparisonItem
|
||||
$bg="rgba(99, 102, 241, 0.04)"
|
||||
$color={designTokens.colors.primary.main}
|
||||
>
|
||||
<div className="item-icon"><ArrowUpOutlined /></div>
|
||||
<div className="item-icon">
|
||||
<ArrowUpOutlined />
|
||||
</div>
|
||||
<div className="item-value">{stats?.outCount || 0}</div>
|
||||
<div className="item-label">出库次数</div>
|
||||
<div className="item-sub">共 <strong>{stats?.outQuantity || 0}</strong> 件</div>
|
||||
<div className="item-sub">
|
||||
共 <strong>{stats?.outQuantity || 0}</strong> 件
|
||||
</div>
|
||||
</ComparisonItem>
|
||||
</InOutComparison>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
style={{
|
||||
marginTop: 20,
|
||||
padding: '18px 24px',
|
||||
style={{
|
||||
marginTop: 20,
|
||||
padding: '18px 24px',
|
||||
background: `linear-gradient(135deg, ${netQuantity >= 0 ? 'rgba(16, 185, 129, 0.08)' : 'rgba(239, 68, 68, 0.08)'} 0%, ${netQuantity >= 0 ? 'rgba(5, 150, 105, 0.04)' : 'rgba(185, 28, 28, 0.04)'} 100%)`,
|
||||
borderRadius: 14,
|
||||
display: 'flex',
|
||||
@@ -1368,14 +1427,22 @@ const ConsumableStatistics = () => {
|
||||
{netQuantity >= 0 ? (
|
||||
<>
|
||||
<RiseOutlined style={{ color: designTokens.colors.success.main, fontSize: 20 }} />
|
||||
<Text style={{ color: designTokens.colors.success.main, fontWeight: 600, fontSize: 15 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: designTokens.colors.success.main,
|
||||
fontWeight: 600,
|
||||
fontSize: 15,
|
||||
}}
|
||||
>
|
||||
净入库 +{netQuantity} 件
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FallOutlined style={{ color: designTokens.colors.error.main, fontSize: 20 }} />
|
||||
<Text style={{ color: designTokens.colors.error.main, fontWeight: 600, fontSize: 15 }}>
|
||||
<Text
|
||||
style={{ color: designTokens.colors.error.main, fontWeight: 600, fontSize: 15 }}
|
||||
>
|
||||
净出库 {Math.abs(netQuantity)} 件
|
||||
</Text>
|
||||
</>
|
||||
@@ -1384,10 +1451,16 @@ const ConsumableStatistics = () => {
|
||||
</div>
|
||||
</BentoCard>
|
||||
|
||||
<BentoCard variants={itemVariants} $col="span 6" $iconBg={designTokens.colors.secondary.gradient}>
|
||||
<BentoCard
|
||||
variants={itemVariants}
|
||||
$col="span 6"
|
||||
$iconBg={designTokens.colors.secondary.gradient}
|
||||
>
|
||||
<div className="card-header">
|
||||
<div className="header-left">
|
||||
<div className="header-icon"><PieChartOutlined /></div>
|
||||
<div className="header-icon">
|
||||
<PieChartOutlined />
|
||||
</div>
|
||||
<span className="header-title">类别分布</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1426,10 +1499,16 @@ const ConsumableStatistics = () => {
|
||||
</BentoGrid>
|
||||
|
||||
<BentoGrid variants={containerVariants} initial="hidden" animate="visible">
|
||||
<BentoCard variants={itemVariants} $col="span 6" $iconBg={designTokens.colors.warning.gradient}>
|
||||
<BentoCard
|
||||
variants={itemVariants}
|
||||
$col="span 6"
|
||||
$iconBg={designTokens.colors.warning.gradient}
|
||||
>
|
||||
<div className="card-header">
|
||||
<div className="header-left">
|
||||
<div className="header-icon"><ExclamationCircleOutlined /></div>
|
||||
<div className="header-icon">
|
||||
<ExclamationCircleOutlined />
|
||||
</div>
|
||||
<span className="header-title">库存预警</span>
|
||||
</div>
|
||||
{lowStockItems.length > 0 ? (
|
||||
@@ -1438,14 +1517,16 @@ const ConsumableStatistics = () => {
|
||||
style={{ backgroundColor: designTokens.colors.warning.main }}
|
||||
/>
|
||||
) : (
|
||||
<span style={{
|
||||
fontSize: 12,
|
||||
color: designTokens.colors.success.main,
|
||||
fontWeight: 500,
|
||||
padding: '4px 10px',
|
||||
background: 'rgba(16, 185, 129, 0.1)',
|
||||
borderRadius: 8
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: designTokens.colors.success.main,
|
||||
fontWeight: 500,
|
||||
padding: '4px 10px',
|
||||
background: 'rgba(16, 185, 129, 0.1)',
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
全部充足
|
||||
</span>
|
||||
)}
|
||||
@@ -1461,15 +1542,18 @@ const ConsumableStatistics = () => {
|
||||
total: lowStockItems.length,
|
||||
showSizeChanger: false,
|
||||
showQuickJumper: false,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (page) => setLowStockPagination(prev => ({ ...prev, current: page })),
|
||||
showTotal: total => `共 ${total} 条`,
|
||||
onChange: page => setLowStockPagination(prev => ({ ...prev, current: page })),
|
||||
}}
|
||||
size="small"
|
||||
scroll={{ x: 'max-content', y: 300 }}
|
||||
locale={{
|
||||
emptyText: (
|
||||
<EmptyState>
|
||||
<BoxPlotOutlined className="empty-icon" style={{ color: designTokens.colors.success.main }} />
|
||||
<BoxPlotOutlined
|
||||
className="empty-icon"
|
||||
style={{ color: designTokens.colors.success.main }}
|
||||
/>
|
||||
<div className="empty-text">库存充足</div>
|
||||
<div className="empty-subtext">所有耗材均在安全范围内</div>
|
||||
</EmptyState>
|
||||
@@ -1479,10 +1563,16 @@ const ConsumableStatistics = () => {
|
||||
</div>
|
||||
</BentoCard>
|
||||
|
||||
<BentoCard variants={itemVariants} $col="span 6" $iconBg={designTokens.colors.success.gradient}>
|
||||
<BentoCard
|
||||
variants={itemVariants}
|
||||
$col="span 6"
|
||||
$iconBg={designTokens.colors.success.gradient}
|
||||
>
|
||||
<div className="card-header">
|
||||
<div className="header-left">
|
||||
<div className="header-icon"><HistoryOutlined /></div>
|
||||
<div className="header-icon">
|
||||
<HistoryOutlined />
|
||||
</div>
|
||||
<span className="header-title">最近记录</span>
|
||||
</div>
|
||||
<div className="header-extra">最近10条</div>
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { Card, Row, Col, Typography, message } from 'antd';
|
||||
import { DatabaseOutlined, CloudServerOutlined, WarningOutlined, HomeOutlined, TeamOutlined, BarChartOutlined, DashboardOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
DatabaseOutlined,
|
||||
CloudServerOutlined,
|
||||
WarningOutlined,
|
||||
HomeOutlined,
|
||||
TeamOutlined,
|
||||
BarChartOutlined,
|
||||
DashboardOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import api from '../api';
|
||||
import { designTokens } from '../config/theme';
|
||||
import { useFetch } from '../hooks/useSWR';
|
||||
@@ -185,22 +193,33 @@ function Dashboard() {
|
||||
let currentAngle = 0;
|
||||
|
||||
if (runningDeg > 0) {
|
||||
gradientParts.push(`${PIE_CHART_COLORS.success} ${currentAngle}deg ${currentAngle + runningDeg}deg`);
|
||||
gradientParts.push(
|
||||
`${PIE_CHART_COLORS.success} ${currentAngle}deg ${currentAngle + runningDeg}deg`
|
||||
);
|
||||
currentAngle += runningDeg;
|
||||
}
|
||||
if (maintenanceDeg > 0) {
|
||||
gradientParts.push(`${PIE_CHART_COLORS.warning} ${currentAngle}deg ${currentAngle + maintenanceDeg}deg`);
|
||||
gradientParts.push(
|
||||
`${PIE_CHART_COLORS.warning} ${currentAngle}deg ${currentAngle + maintenanceDeg}deg`
|
||||
);
|
||||
currentAngle += maintenanceDeg;
|
||||
}
|
||||
if (faultDeg > 0) {
|
||||
gradientParts.push(`${PIE_CHART_COLORS.error} ${currentAngle}deg ${currentAngle + faultDeg}deg`);
|
||||
gradientParts.push(
|
||||
`${PIE_CHART_COLORS.error} ${currentAngle}deg ${currentAngle + faultDeg}deg`
|
||||
);
|
||||
currentAngle += faultDeg;
|
||||
}
|
||||
if (offlineDeg > 0) {
|
||||
gradientParts.push(`${PIE_CHART_COLORS.primary} ${currentAngle}deg ${currentAngle + offlineDeg}deg`);
|
||||
gradientParts.push(
|
||||
`${PIE_CHART_COLORS.primary} ${currentAngle}deg ${currentAngle + offlineDeg}deg`
|
||||
);
|
||||
}
|
||||
|
||||
const conicGradient = gradientParts.length > 0 ? `conic-gradient(${gradientParts.join(', ')})` : 'conic-gradient(#e5e7eb 0deg 360deg)';
|
||||
const conicGradient =
|
||||
gradientParts.length > 0
|
||||
? `conic-gradient(${gradientParts.join(', ')})`
|
||||
: 'conic-gradient(#e5e7eb 0deg 360deg)';
|
||||
|
||||
return {
|
||||
width: '180px',
|
||||
@@ -215,11 +234,11 @@ function Dashboard() {
|
||||
}, [deviceStatusPercentages]);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setAnimatedKey((prev) => prev + 1);
|
||||
setAnimatedKey(prev => prev + 1);
|
||||
await mutateStats();
|
||||
}, [mutateStats]);
|
||||
|
||||
const handleHover = useCallback((key) => {
|
||||
const handleHover = useCallback(key => {
|
||||
setHoveredCard(key);
|
||||
}, []);
|
||||
|
||||
@@ -325,17 +344,14 @@ function Dashboard() {
|
||||
[stats.deviceGrowth, stats.faultTrend, stats.userGrowth, stats.ticketTrend]
|
||||
);
|
||||
|
||||
const deviceTrendData = useMemo(
|
||||
() => {
|
||||
const rawData = statsData?.deviceTrendData || [];
|
||||
return rawData.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
color: designTokens.colors.primary.main
|
||||
}));
|
||||
},
|
||||
[statsData?.deviceTrendData]
|
||||
);
|
||||
const deviceTrendData = useMemo(() => {
|
||||
const rawData = statsData?.deviceTrendData || [];
|
||||
return rawData.map(item => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
color: designTokens.colors.primary.main,
|
||||
}));
|
||||
}, [statsData?.deviceTrendData]);
|
||||
|
||||
const styles = `
|
||||
@keyframes fadeInDown {
|
||||
@@ -375,7 +391,7 @@ function Dashboard() {
|
||||
</div>
|
||||
|
||||
<Row gutter={[24, 24]} style={{ marginBottom: '32px' }}>
|
||||
{statCards.map((config) => (
|
||||
{statCards.map(config => (
|
||||
<StatCard
|
||||
key={config.statKey}
|
||||
config={config}
|
||||
@@ -544,7 +560,11 @@ function Dashboard() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<QuickStats onlineRate={stats.onlineRate} powerUsage={stats.powerUsage} totalMaxPower={stats.totalMaxPower} />
|
||||
<QuickStats
|
||||
onlineRate={stats.onlineRate}
|
||||
powerUsage={stats.powerUsage}
|
||||
totalMaxPower={stats.totalMaxPower}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={8}>
|
||||
|
||||
@@ -45,51 +45,51 @@ const OptionsEditor = ({ value = [], onChange }) => {
|
||||
};
|
||||
|
||||
const handleUpdate = (index, field, fieldValue) => {
|
||||
const newOptions = value.map((opt, i) =>
|
||||
i === index ? { ...opt, [field]: fieldValue } : opt
|
||||
);
|
||||
const newOptions = value.map((opt, i) => (i === index ? { ...opt, [field]: fieldValue } : opt));
|
||||
onChange(newOptions);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
background: 'linear-gradient(135deg, #fafbfc 0%, #f5f7fa 100%)',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: '16px',
|
||||
gap: '8px',
|
||||
}}>
|
||||
<div style={{
|
||||
width: '4px',
|
||||
height: '16px',
|
||||
background: 'linear-gradient(180deg, #667eea 0%, #764ba2 100%)',
|
||||
borderRadius: '2px',
|
||||
}}/>
|
||||
<span style={{ color: '#333', fontSize: '14px', fontWeight: '600' }}>
|
||||
选项配置
|
||||
</span>
|
||||
<span style={{ color: '#999', fontSize: '12px' }}>
|
||||
(值用于提交,标签用于显示)
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
background: 'linear-gradient(135deg, #fafbfc 0%, #f5f7fa 100%)',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: '16px',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '4px',
|
||||
height: '16px',
|
||||
background: 'linear-gradient(180deg, #667eea 0%, #764ba2 100%)',
|
||||
borderRadius: '2px',
|
||||
}}
|
||||
/>
|
||||
<span style={{ color: '#333', fontSize: '14px', fontWeight: '600' }}>选项配置</span>
|
||||
<span style={{ color: '#999', fontSize: '12px' }}>(值用于提交,标签用于显示)</span>
|
||||
</div>
|
||||
|
||||
{value.length === 0 ? (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '24px',
|
||||
background: '#fff',
|
||||
borderRadius: '8px',
|
||||
border: '1px dashed #d9d9d9',
|
||||
}}>
|
||||
<div style={{ color: '#bbb', fontSize: '14px', marginBottom: '12px' }}>
|
||||
暂无选项
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: '24px',
|
||||
background: '#fff',
|
||||
borderRadius: '8px',
|
||||
border: '1px dashed #d9d9d9',
|
||||
}}
|
||||
>
|
||||
<div style={{ color: '#bbb', fontSize: '14px', marginBottom: '12px' }}>暂无选项</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusCircleOutlined />}
|
||||
@@ -105,15 +105,23 @@ const OptionsEditor = ({ value = [], onChange }) => {
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '16px' }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
padding: '0 4px',
|
||||
marginBottom: '4px',
|
||||
}}>
|
||||
<span style={{ width: '160px', color: '#666', fontSize: '12px', fontWeight: '500' }}>值(value)</span>
|
||||
<span style={{ width: '160px', color: '#666', fontSize: '12px', fontWeight: '500' }}>标签(label)</span>
|
||||
<div
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '16px' }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
padding: '0 4px',
|
||||
marginBottom: '4px',
|
||||
}}
|
||||
>
|
||||
<span style={{ width: '160px', color: '#666', fontSize: '12px', fontWeight: '500' }}>
|
||||
值(value)
|
||||
</span>
|
||||
<span style={{ width: '160px', color: '#666', fontSize: '12px', fontWeight: '500' }}>
|
||||
标签(label)
|
||||
</span>
|
||||
</div>
|
||||
{value.map((opt, index) => (
|
||||
<div
|
||||
@@ -129,19 +137,21 @@ const OptionsEditor = ({ value = [], onChange }) => {
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #667eea20 0%, #764ba220 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#667eea',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #667eea20 0%, #764ba220 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#667eea',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
<Input
|
||||
|
||||
@@ -69,11 +69,7 @@ import {
|
||||
BatchStatusModal,
|
||||
} from '../components/device';
|
||||
import { useDebounce } from '../hooks/useDebounce';
|
||||
import {
|
||||
getDeviceTypeIcon,
|
||||
getStatusConfig,
|
||||
processDeviceData,
|
||||
} from '../utils/deviceUtils.jsx';
|
||||
import { getDeviceTypeIcon, getStatusConfig, processDeviceData } from '../utils/deviceUtils.jsx';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
@@ -165,7 +161,7 @@ function DeviceManagement() {
|
||||
} else {
|
||||
setAllDevices(processedDevices);
|
||||
}
|
||||
setPagination((prev) => ({ ...prev, current: page, pageSize, total }));
|
||||
setPagination(prev => ({ ...prev, current: page, pageSize, total }));
|
||||
hasMoreRef.current = page * pageSize < total;
|
||||
} catch (error) {
|
||||
message.error('获取设备列表失败');
|
||||
@@ -183,7 +179,7 @@ function DeviceManagement() {
|
||||
setLoadingFields(true);
|
||||
const response = await axios.get('/api/deviceFields');
|
||||
let fields = response.data.sort((a, b) => a.order - b.order);
|
||||
|
||||
|
||||
// 补充缺失的 options(状态和设备类型)
|
||||
fields = fields.map(field => {
|
||||
if (field.fieldName === 'type' && !field.options) {
|
||||
@@ -191,12 +187,14 @@ function DeviceManagement() {
|
||||
return { ...field, options: defaultTypeField?.options || [] };
|
||||
}
|
||||
if (field.fieldName === 'status' && !field.options) {
|
||||
const defaultStatusField = DEFAULT_DEVICE_FIELDS_LOCAL.find(f => f.fieldName === 'status');
|
||||
const defaultStatusField = DEFAULT_DEVICE_FIELDS_LOCAL.find(
|
||||
f => f.fieldName === 'status'
|
||||
);
|
||||
return { ...field, options: defaultStatusField?.options || [] };
|
||||
}
|
||||
return field;
|
||||
});
|
||||
|
||||
|
||||
setDeviceFields(fields);
|
||||
} catch (error) {
|
||||
message.error('获取字段配置失败');
|
||||
@@ -275,7 +273,13 @@ function DeviceManagement() {
|
||||
|
||||
const handleLoadMoreDevices = useCallback(() => {
|
||||
if (!hasMoreRef.current || isLoadingRef.current || deviceLoadingMore) return;
|
||||
if (debouncedKeyword || status !== 'all' || type !== 'all' || roomId !== 'all' || rackId !== 'all') {
|
||||
if (
|
||||
debouncedKeyword ||
|
||||
status !== 'all' ||
|
||||
type !== 'all' ||
|
||||
roomId !== 'all' ||
|
||||
rackId !== 'all'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
isLoadingRef.current = true;
|
||||
@@ -283,7 +287,17 @@ function DeviceManagement() {
|
||||
fetchDevices(nextPage, pagination.pageSize, true).then(() => {
|
||||
isLoadingRef.current = false;
|
||||
});
|
||||
}, [pagination.current, pagination.pageSize, debouncedKeyword, status, type, roomId, rackId, deviceLoadingMore, fetchDevices]);
|
||||
}, [
|
||||
pagination.current,
|
||||
pagination.pageSize,
|
||||
debouncedKeyword,
|
||||
status,
|
||||
type,
|
||||
roomId,
|
||||
rackId,
|
||||
deviceLoadingMore,
|
||||
fetchDevices,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loadMoreRef.current) return;
|
||||
@@ -291,7 +305,13 @@ function DeviceManagement() {
|
||||
const observer = new IntersectionObserver(
|
||||
entries => {
|
||||
if (entries[0].isIntersecting && hasMoreRef.current && !deviceLoadingMore) {
|
||||
if (!debouncedKeyword && status === 'all' && type === 'all' && roomId === 'all' && rackId === 'all') {
|
||||
if (
|
||||
!debouncedKeyword &&
|
||||
status === 'all' &&
|
||||
type === 'all' &&
|
||||
roomId === 'all' &&
|
||||
rackId === 'all'
|
||||
) {
|
||||
handleLoadMoreDevices();
|
||||
}
|
||||
}
|
||||
@@ -316,7 +336,7 @@ function DeviceManagement() {
|
||||
setEditingDevice(null);
|
||||
};
|
||||
|
||||
const handleSubmit = async (deviceData) => {
|
||||
const handleSubmit = async deviceData => {
|
||||
try {
|
||||
if (editingDevice) {
|
||||
await axios.put(`/api/devices/${editingDevice.deviceId}`, deviceData);
|
||||
@@ -336,7 +356,7 @@ function DeviceManagement() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (values) => {
|
||||
const handleSearch = values => {
|
||||
setSearching(true);
|
||||
|
||||
setKeyword(values.keyword || '');
|
||||
@@ -345,7 +365,7 @@ function DeviceManagement() {
|
||||
setRoomId(values.roomId || 'all');
|
||||
setRackId(values.rackId || 'all');
|
||||
|
||||
setPagination((prev) => ({ ...prev, current: 1 }));
|
||||
setPagination(prev => ({ ...prev, current: 1 }));
|
||||
|
||||
setTimeout(() => setSearching(false), 300);
|
||||
};
|
||||
@@ -363,7 +383,7 @@ function DeviceManagement() {
|
||||
setTimeout(() => setSearching(false), 300);
|
||||
};
|
||||
|
||||
const handleTableChange = (newPagination) => {
|
||||
const handleTableChange = newPagination => {
|
||||
setPagination(newPagination);
|
||||
|
||||
const start = (newPagination.current - 1) * newPagination.pageSize;
|
||||
@@ -453,7 +473,7 @@ function DeviceManagement() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = async (deviceId) => {
|
||||
const handleDelete = async deviceId => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个设备吗?',
|
||||
@@ -473,18 +493,18 @@ function DeviceManagement() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleShowDetail = (device) => {
|
||||
const handleShowDetail = device => {
|
||||
setSelectedDevice(device);
|
||||
setDetailModalVisible(true);
|
||||
};
|
||||
|
||||
const handleViewDeviceTickets = (device) => {
|
||||
const handleViewDeviceTickets = device => {
|
||||
navigate(
|
||||
`/tickets?deviceId=${device.deviceId}&deviceName=${encodeURIComponent(device.name)}&serialNumber=${encodeURIComponent(device.serialNumber || '')}&view=true`
|
||||
);
|
||||
};
|
||||
|
||||
const handleCreateTicketForDevice = (device) => {
|
||||
const handleCreateTicketForDevice = device => {
|
||||
navigate(
|
||||
`/tickets?deviceId=${device.deviceId}&deviceName=${encodeURIComponent(device.name)}&serialNumber=${encodeURIComponent(device.serialNumber || '')}&create=true`
|
||||
);
|
||||
@@ -498,7 +518,7 @@ function DeviceManagement() {
|
||||
setBatchStatusModalVisible(true);
|
||||
};
|
||||
|
||||
const handleBatchStatusChange = async (newStatus) => {
|
||||
const handleBatchStatusChange = async newStatus => {
|
||||
setBatchStatusLoading(true);
|
||||
try {
|
||||
const response = await axios.put('/api/devices/batch-status', {
|
||||
@@ -532,9 +552,9 @@ function DeviceManagement() {
|
||||
if (scope === 'selected') {
|
||||
deviceIds = selectedDevices;
|
||||
} else if (scope === 'currentPage') {
|
||||
deviceIds = allDevices.map((device) => device.deviceId);
|
||||
deviceIds = allDevices.map(device => device.deviceId);
|
||||
} else if (scope === 'all') {
|
||||
deviceIds = allDevices.map((device) => device.deviceId);
|
||||
deviceIds = allDevices.map(device => device.deviceId);
|
||||
}
|
||||
|
||||
if (deviceIds.length === 0) {
|
||||
@@ -543,7 +563,7 @@ function DeviceManagement() {
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
deviceIds.forEach((id) => params.append('deviceIds', id));
|
||||
deviceIds.forEach(id => params.append('deviceIds', id));
|
||||
params.append('format', format);
|
||||
|
||||
const response = await axios.get(`/api/devices/enhanced-export?${params.toString()}`, {
|
||||
@@ -575,7 +595,7 @@ function DeviceManagement() {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
onUploadProgress: progressEvent => {
|
||||
const progress = Math.round((progressEvent.loaded * 50) / progressEvent.total);
|
||||
callbacks.onProgress(Math.min(progress, 50), '正在上传文件...');
|
||||
},
|
||||
@@ -617,7 +637,7 @@ function DeviceManagement() {
|
||||
Array.isArray(data.errors) &&
|
||||
data.errors.length > 0
|
||||
) {
|
||||
errorDetails = data.errors.map((err) => ({
|
||||
errorDetails = data.errors.map(err => ({
|
||||
row: err.row || 0,
|
||||
error: err.error || err.message || '服务器内部错误',
|
||||
}));
|
||||
@@ -645,7 +665,7 @@ function DeviceManagement() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveFieldConfig = async (updatedFields) => {
|
||||
const handleSaveFieldConfig = async updatedFields => {
|
||||
try {
|
||||
const response = await axios.post('/api/deviceFields/config', updatedFields);
|
||||
setDeviceFields(response.data);
|
||||
@@ -657,26 +677,26 @@ function DeviceManagement() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetFieldConfig = (defaultFields) => {
|
||||
const handleResetFieldConfig = defaultFields => {
|
||||
setDeviceFields(defaultFields);
|
||||
};
|
||||
|
||||
const handleSelectionChange = (selectedRowKeys) => {
|
||||
const handleSelectionChange = selectedRowKeys => {
|
||||
setSelectedDevices(selectedRowKeys);
|
||||
setSelectAll(selectedRowKeys.length === allDevices.length && allDevices.length > 0);
|
||||
};
|
||||
|
||||
const handleHeaderCellResize = (key) => (column) => ({
|
||||
const handleHeaderCellResize = key => column => ({
|
||||
width: column.width,
|
||||
onResize: (width) => {
|
||||
setColumnWidths((prev) => ({ ...prev, [key]: width }));
|
||||
onResize: width => {
|
||||
setColumnWidths(prev => ({ ...prev, [key]: width }));
|
||||
},
|
||||
});
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const generatedColumns = [];
|
||||
|
||||
deviceFields.forEach((field) => {
|
||||
deviceFields.forEach(field => {
|
||||
if (!field.visible) return;
|
||||
|
||||
if (field.fieldName === 'rackId') {
|
||||
@@ -701,7 +721,7 @@ function DeviceManagement() {
|
||||
key: field.fieldName,
|
||||
width: columnWidths[field.fieldName] || 100,
|
||||
onHeaderCell: handleHeaderCellResize(field.fieldName),
|
||||
render: (type) => (
|
||||
render: type => (
|
||||
<Space>
|
||||
{getDeviceTypeIcon(type)}
|
||||
<span>{TYPE_MAP[type]}</span>
|
||||
@@ -715,7 +735,7 @@ function DeviceManagement() {
|
||||
key: field.fieldName,
|
||||
width: columnWidths[field.fieldName] || 90,
|
||||
onHeaderCell: handleHeaderCellResize(field.fieldName),
|
||||
render: (status) => {
|
||||
render: status => {
|
||||
const config = STATUS_MAP[status] || { text: status, color: 'default' };
|
||||
const statusStyles = {
|
||||
running: { bg: '#f6ffed', border: '#52c41a', text: '#389e0d' },
|
||||
@@ -724,7 +744,11 @@ function DeviceManagement() {
|
||||
fault: { bg: '#fff2f0', border: '#ff4d4f', text: '#cf1322' },
|
||||
idle: { bg: '#E6FFFA', border: '#36cfc9', text: '#08979d' },
|
||||
};
|
||||
const style = statusStyles[status] || { bg: '#fafafa', border: '#d9d9d9', text: '#595959' };
|
||||
const style = statusStyles[status] || {
|
||||
bg: '#fafafa',
|
||||
border: '#d9d9d9',
|
||||
text: '#595959',
|
||||
};
|
||||
return (
|
||||
<Tag
|
||||
style={{
|
||||
@@ -752,7 +776,7 @@ function DeviceManagement() {
|
||||
key: field.fieldName,
|
||||
width: columnWidths[field.fieldName] || 120,
|
||||
onHeaderCell: handleHeaderCellResize(field.fieldName),
|
||||
render: (date) => {
|
||||
render: date => {
|
||||
if (!date) return '';
|
||||
|
||||
const dateObj = new Date(date);
|
||||
@@ -809,8 +833,8 @@ function DeviceManagement() {
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.target.style.textDecoration = 'underline')}
|
||||
onMouseLeave={(e) => (e.target.style.textDecoration = 'none')}
|
||||
onMouseEnter={e => (e.target.style.textDecoration = 'underline')}
|
||||
onMouseLeave={e => (e.target.style.textDecoration = 'none')}
|
||||
>
|
||||
{value || '-'}
|
||||
</a>
|
||||
@@ -993,17 +1017,38 @@ function DeviceManagement() {
|
||||
style={{ width: '100%' }}
|
||||
className="filter-form"
|
||||
>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: designTokens.spacing.md, alignItems: 'center', width: '100%' }}>
|
||||
<div style={{
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: designTokens.spacing.md,
|
||||
alignItems: 'center',
|
||||
gap: designTokens.spacing.sm,
|
||||
padding: '0 12px',
|
||||
borderRight: `1px solid ${designTokens.colors.border.light}`,
|
||||
marginRight: 4,
|
||||
}}>
|
||||
<FilterOutlined style={{ color: designTokens.colors.primary.main, fontSize: '16px' }} />
|
||||
<span style={{ fontSize: '13px', fontWeight: 500, color: designTokens.colors.text.secondary, whiteSpace: 'nowrap' }}>筛选</span>
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: designTokens.spacing.sm,
|
||||
padding: '0 12px',
|
||||
borderRight: `1px solid ${designTokens.colors.border.light}`,
|
||||
marginRight: 4,
|
||||
}}
|
||||
>
|
||||
<FilterOutlined
|
||||
style={{ color: designTokens.colors.primary.main, fontSize: '16px' }}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
color: designTokens.colors.text.secondary,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
筛选
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Form.Item name="keyword" style={{ margin: 0 }}>
|
||||
@@ -1017,7 +1062,7 @@ function DeviceManagement() {
|
||||
transition: `all ${designTokens.transitions.fast}`,
|
||||
}}
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
onChange={e => setKeyword(e.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -1064,10 +1109,14 @@ function DeviceManagement() {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
color: advancedSearchVisible ? designTokens.colors.primary.main : designTokens.colors.text.secondary,
|
||||
color: advancedSearchVisible
|
||||
? designTokens.colors.primary.main
|
||||
: designTokens.colors.text.secondary,
|
||||
fontWeight: 500,
|
||||
borderRadius: designTokens.borderRadius.medium,
|
||||
background: advancedSearchVisible ? `${designTokens.colors.primary.main}10` : 'transparent',
|
||||
background: advancedSearchVisible
|
||||
? `${designTokens.colors.primary.main}10`
|
||||
: 'transparent',
|
||||
transition: `all ${designTokens.transitions.fast}`,
|
||||
}}
|
||||
>
|
||||
@@ -1117,30 +1166,43 @@ function DeviceManagement() {
|
||||
</div>
|
||||
|
||||
{advancedSearchVisible && (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: designTokens.spacing.md,
|
||||
padding: `${designTokens.spacing.md}px 0`,
|
||||
borderTop: `1px dashed ${designTokens.colors.border.light}`,
|
||||
marginTop: designTokens.spacing.sm,
|
||||
}}>
|
||||
<div style={{
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: designTokens.spacing.sm,
|
||||
padding: '0 12px',
|
||||
borderRight: `1px solid ${designTokens.colors.border.light}`,
|
||||
marginRight: 4,
|
||||
}}>
|
||||
flexWrap: 'wrap',
|
||||
gap: designTokens.spacing.md,
|
||||
padding: `${designTokens.spacing.md}px 0`,
|
||||
borderTop: `1px dashed ${designTokens.colors.border.light}`,
|
||||
marginTop: designTokens.spacing.sm,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: designTokens.spacing.sm,
|
||||
padding: '0 12px',
|
||||
borderRight: `1px solid ${designTokens.colors.border.light}`,
|
||||
marginRight: 4,
|
||||
}}
|
||||
>
|
||||
<EnvironmentOutlined style={{ color: '#3b82f6', fontSize: '16px' }} />
|
||||
<span style={{ fontSize: '13px', fontWeight: 500, color: designTokens.colors.text.secondary, whiteSpace: 'nowrap' }}>位置筛选</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
color: designTokens.colors.text.secondary,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
位置筛选
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Form.Item name="roomId" style={{ margin: 0 }}>
|
||||
<Select
|
||||
value={roomId}
|
||||
onChange={(value) => {
|
||||
onChange={value => {
|
||||
setRoomId(value);
|
||||
setRackId('all');
|
||||
}}
|
||||
@@ -1149,7 +1211,7 @@ function DeviceManagement() {
|
||||
suffixIcon={<EnvironmentOutlined style={{ color: '#3b82f6' }} />}
|
||||
>
|
||||
<Option value="all">所有机房</Option>
|
||||
{rooms.map((room) => (
|
||||
{rooms.map(room => (
|
||||
<Option key={room.roomId} value={room.roomId}>
|
||||
{room.name}
|
||||
</Option>
|
||||
@@ -1169,8 +1231,8 @@ function DeviceManagement() {
|
||||
>
|
||||
<Option value="all">所有机柜</Option>
|
||||
{racks
|
||||
.filter((rack) => roomId === 'all' || rack.roomId === roomId)
|
||||
.map((rack) => (
|
||||
.filter(rack => roomId === 'all' || rack.roomId === roomId)
|
||||
.map(rack => (
|
||||
<Option key={rack.rackId} value={rack.rackId}>
|
||||
{rack.name}
|
||||
</Option>
|
||||
@@ -1217,7 +1279,7 @@ function DeviceManagement() {
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
pageSizeOptions: ['10', '20', '30', '50', '100'],
|
||||
showTotal: (total) => `共 ${total} 条记录`,
|
||||
showTotal: total => `共 ${total} 条记录`,
|
||||
style: { marginTop: '16px' },
|
||||
}}
|
||||
onChange={handleTableChange}
|
||||
@@ -1243,7 +1305,7 @@ function DeviceManagement() {
|
||||
key: 'all',
|
||||
text: '全选',
|
||||
onSelect: () => {
|
||||
const allIds = allDevices.map((device) => device.deviceId);
|
||||
const allIds = allDevices.map(device => device.deviceId);
|
||||
setSelectedDevices(allIds);
|
||||
setSelectAll(true);
|
||||
},
|
||||
@@ -1252,8 +1314,8 @@ function DeviceManagement() {
|
||||
key: 'invert',
|
||||
text: '反选',
|
||||
onSelect: () => {
|
||||
const visibleIds = allDevices.map((device) => device.deviceId);
|
||||
const newSelected = visibleIds.filter((id) => !selectedDevices.includes(id));
|
||||
const visibleIds = allDevices.map(device => device.deviceId);
|
||||
const newSelected = visibleIds.filter(id => !selectedDevices.includes(id));
|
||||
setSelectedDevices(newSelected);
|
||||
setSelectAll(newSelected.length === allDevices.length);
|
||||
},
|
||||
@@ -1268,11 +1330,11 @@ function DeviceManagement() {
|
||||
},
|
||||
],
|
||||
}}
|
||||
onRow={(record) => ({
|
||||
onRow={record => ({
|
||||
onClick: () =>
|
||||
handleSelectionChange(
|
||||
selectedDevices.includes(record.deviceId)
|
||||
? selectedDevices.filter((id) => id !== record.deviceId)
|
||||
? selectedDevices.filter(id => id !== record.deviceId)
|
||||
: [...selectedDevices, record.deviceId]
|
||||
),
|
||||
})}
|
||||
@@ -1283,11 +1345,16 @@ function DeviceManagement() {
|
||||
return index % 2 === 0 ? 'ant-table-row-even' : 'ant-table-row-odd';
|
||||
}}
|
||||
/>
|
||||
{hasMoreRef.current && !debouncedKeyword && status === 'all' && type === 'all' && roomId === 'all' && rackId === 'all' && (
|
||||
<div ref={loadMoreRef} style={{ textAlign: 'center', padding: '20px' }}>
|
||||
{deviceLoadingMore && <Spin tip="加载更多设备..." />}
|
||||
</div>
|
||||
)}
|
||||
{hasMoreRef.current &&
|
||||
!debouncedKeyword &&
|
||||
status === 'all' &&
|
||||
type === 'all' &&
|
||||
roomId === 'all' &&
|
||||
rackId === 'all' && (
|
||||
<div ref={loadMoreRef} style={{ textAlign: 'center', padding: '20px' }}>
|
||||
{deviceLoadingMore && <Spin tip="加载更多设备..." />}
|
||||
</div>
|
||||
)}
|
||||
{!hasMoreRef.current && allDevices.length > 0 && (
|
||||
<div style={{ textAlign: 'center', padding: '16px', color: '#999' }}>
|
||||
已加载全部 {pagination.total} 个设备
|
||||
|
||||
@@ -38,9 +38,7 @@ function ErrorBoundaryTest() {
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
<h1 style={{ fontSize: '24px', marginBottom: '8px' }}>
|
||||
🧪 错误边界测试页面
|
||||
</h1>
|
||||
<h1 style={{ fontSize: '24px', marginBottom: '8px' }}>🧪 错误边界测试页面</h1>
|
||||
<p style={{ opacity: 0.9 }}>
|
||||
用于测试 React 错误边界(Error Boundary)功能,确保单个组件错误不会导致整个页面崩溃
|
||||
</p>
|
||||
@@ -62,11 +60,7 @@ function ErrorBoundaryTest() {
|
||||
|
||||
<Card title="测试 2:页面级别错误边界" size="small">
|
||||
<p>这个测试会触发页面级别的错误边界</p>
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
onClick={() => setShowError(true)}
|
||||
>
|
||||
<Button type="primary" danger onClick={() => setShowError(true)}>
|
||||
触发页面错误
|
||||
</Button>
|
||||
{showError && <BrokenComponent />}
|
||||
@@ -95,9 +89,7 @@ function ErrorBoundaryTest() {
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<h3 style={{ color: '#ff4d4f', margin: '0 0 8px 0' }}>
|
||||
⚠️ 自定义错误提示
|
||||
</h3>
|
||||
<h3 style={{ color: '#ff4d4f', margin: '0 0 8px 0' }}>⚠️ 自定义错误提示</h3>
|
||||
<p style={{ margin: 0, color: '#666' }}>
|
||||
这是自定义的 fallback UI,当组件出错时会显示这个界面
|
||||
</p>
|
||||
@@ -131,16 +123,15 @@ function ErrorBoundaryTest() {
|
||||
<Card title="使用说明" size="small">
|
||||
<ul style={{ lineHeight: '2' }}>
|
||||
<li>
|
||||
<strong>错误边界(Error Boundary)</strong>:
|
||||
是 React 提供的错误处理机制,可以捕获子组件树中的 JavaScript 错误
|
||||
<strong>错误边界(Error Boundary)</strong>: 是 React
|
||||
提供的错误处理机制,可以捕获子组件树中的 JavaScript 错误
|
||||
</li>
|
||||
<li>
|
||||
<strong>作用</strong>:
|
||||
防止单个组件的错误导致整个应用崩溃,提供友好的错误提示界面
|
||||
<strong>作用</strong>: 防止单个组件的错误导致整个应用崩溃,提供友好的错误提示界面
|
||||
</li>
|
||||
<li>
|
||||
<strong>实现方式</strong>:
|
||||
使用 React.Component 的 componentDidCatch 和 getDerivedStateFromError 生命周期方法
|
||||
<strong>实现方式</strong>: 使用 React.Component 的 componentDidCatch 和
|
||||
getDerivedStateFromError 生命周期方法
|
||||
</li>
|
||||
<li>
|
||||
<strong>注意事项</strong>:
|
||||
|
||||
@@ -68,7 +68,7 @@ const IdleDeviceManagement = () => {
|
||||
};
|
||||
const response = await axios.get('/api/idle-devices', { params });
|
||||
setIdleDevices(response.data.idleDevices || []);
|
||||
setPagination((prev) => ({
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
total: response.data.total || 0,
|
||||
}));
|
||||
@@ -113,7 +113,7 @@ const IdleDeviceManagement = () => {
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (record) => {
|
||||
const handleEdit = record => {
|
||||
setEditingDevice(record);
|
||||
let roomId = null;
|
||||
if (record.rackId && record.Rack) {
|
||||
@@ -136,7 +136,7 @@ const IdleDeviceManagement = () => {
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (deviceId) => {
|
||||
const handleDelete = async deviceId => {
|
||||
try {
|
||||
await axios.delete(`/api/idle-devices/${deviceId}`);
|
||||
message.success('删除成功');
|
||||
@@ -146,7 +146,7 @@ const IdleDeviceManagement = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleShelve = (record) => {
|
||||
const handleShelve = record => {
|
||||
setShelvingDevice(record);
|
||||
setShelvePositionConflict(null);
|
||||
setShelveSelectedRackId(null);
|
||||
@@ -195,7 +195,7 @@ const IdleDeviceManagement = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleShelveRackChange = (value) => {
|
||||
const handleShelveRackChange = value => {
|
||||
setShelveSelectedRackId(value);
|
||||
const position = shelveForm.getFieldValue('position');
|
||||
const height = shelveForm.getFieldValue('height');
|
||||
@@ -206,7 +206,7 @@ const IdleDeviceManagement = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleShelvePositionChange = (e) => {
|
||||
const handleShelvePositionChange = e => {
|
||||
const value = e.target.value ? parseInt(e.target.value) : null;
|
||||
const height = shelveForm.getFieldValue('height');
|
||||
if (shelveSelectedRackId && value) {
|
||||
@@ -216,7 +216,7 @@ const IdleDeviceManagement = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleShelveHeightChange = (e) => {
|
||||
const handleShelveHeightChange = e => {
|
||||
const value = e.target.value ? parseInt(e.target.value) : null;
|
||||
const position = shelveForm.getFieldValue('position');
|
||||
if (shelveSelectedRackId && position) {
|
||||
@@ -279,7 +279,7 @@ const IdleDeviceManagement = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const getIdleDays = (idleDate) => {
|
||||
const getIdleDays = idleDate => {
|
||||
if (!idleDate) return 0;
|
||||
const diff = new Date() - new Date(idleDate);
|
||||
return Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
@@ -292,7 +292,10 @@ const IdleDeviceManagement = () => {
|
||||
width: 60,
|
||||
align: 'center',
|
||||
render: (_, __, index) => (
|
||||
<Badge count={index + 1 + (pagination.current - 1) * pagination.pageSize} style={{ backgroundColor: '#f59e0b' }} />
|
||||
<Badge
|
||||
count={index + 1 + (pagination.current - 1) * pagination.pageSize}
|
||||
style={{ backgroundColor: '#f59e0b' }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -302,16 +305,36 @@ const IdleDeviceManagement = () => {
|
||||
render: (_, record) => (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Avatar
|
||||
style={{ backgroundColor: record.type === 'server' ? '#3b82f6' : record.type === 'switch' ? '#8b5cf6' : '#64748b' }}
|
||||
style={{
|
||||
backgroundColor:
|
||||
record.type === 'server'
|
||||
? '#3b82f6'
|
||||
: record.type === 'switch'
|
||||
? '#8b5cf6'
|
||||
: '#64748b',
|
||||
}}
|
||||
icon={<InboxOutlined />}
|
||||
/>
|
||||
<div>
|
||||
<Text strong style={{ fontSize: '14px', display: 'block' }}>{record.name || '-'}</Text>
|
||||
<Text strong style={{ fontSize: '14px', display: 'block' }}>
|
||||
{record.name || '-'}
|
||||
</Text>
|
||||
<Space size={4}>
|
||||
<Tag color={record.type === 'server' ? 'blue' : record.type === 'switch' ? 'purple' : 'default'} style={{ marginRight: 0 }}>
|
||||
<Tag
|
||||
color={
|
||||
record.type === 'server'
|
||||
? 'blue'
|
||||
: record.type === 'switch'
|
||||
? 'purple'
|
||||
: 'default'
|
||||
}
|
||||
style={{ marginRight: 0 }}
|
||||
>
|
||||
{record.type === 'server' ? '服务器' : record.type === 'switch' ? '交换机' : '其他'}
|
||||
</Tag>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>{record.model || '-'}</Text>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
{record.model || '-'}
|
||||
</Text>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
@@ -322,8 +345,10 @@ const IdleDeviceManagement = () => {
|
||||
dataIndex: 'deviceId',
|
||||
key: 'deviceId',
|
||||
width: 100,
|
||||
render: (text) => (
|
||||
<Text code style={{ fontSize: '12px', padding: '2px 6px' }}>{text}</Text>
|
||||
render: text => (
|
||||
<Text code style={{ fontSize: '12px', padding: '2px 6px' }}>
|
||||
{text}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -340,7 +365,13 @@ const IdleDeviceManagement = () => {
|
||||
);
|
||||
}
|
||||
if (record.sourceType === 'rack' && record.Rack) {
|
||||
const location = [record.Rack.Room?.name, record.Rack.name, record.position ? `U${record.position}` : null].filter(Boolean).join(' / ');
|
||||
const location = [
|
||||
record.Rack.Room?.name,
|
||||
record.Rack.name,
|
||||
record.position ? `U${record.position}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' / ');
|
||||
return (
|
||||
<Space>
|
||||
<InboxOutlined style={{ color: '#3b82f6' }} />
|
||||
@@ -363,7 +394,9 @@ const IdleDeviceManagement = () => {
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Text style={{ color, fontWeight: 600, fontSize: '16px' }}>{days}</Text>
|
||||
<br />
|
||||
<Text type="secondary" style={{ fontSize: '11px' }}>天</Text>
|
||||
<Text type="secondary" style={{ fontSize: '11px' }}>
|
||||
天
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -374,9 +407,11 @@ const IdleDeviceManagement = () => {
|
||||
key: 'idleReason',
|
||||
width: 140,
|
||||
ellipsis: true,
|
||||
render: (text) => (
|
||||
render: text => (
|
||||
<Tooltip title={text || '-'}>
|
||||
<Text type="secondary" ellipsis>{text || '-'}</Text>
|
||||
<Text type="secondary" ellipsis>
|
||||
{text || '-'}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
@@ -386,7 +421,7 @@ const IdleDeviceManagement = () => {
|
||||
key: 'sourceType',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
render: (type) => (
|
||||
render: type => (
|
||||
<Tag color={type === 'warehouse' ? 'green' : 'blue'}>
|
||||
{type === 'warehouse' ? '库房' : '机架'}
|
||||
</Tag>
|
||||
@@ -409,11 +444,27 @@ const IdleDeviceManagement = () => {
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="编辑">
|
||||
<Button type="text" icon={<EditOutlined />} onClick={() => handleEdit(record)} style={{ borderRadius: '6px', color: '#3b82f6' }} />
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
style={{ borderRadius: '6px', color: '#3b82f6' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.deviceId)} okText="确认" cancelText="取消" okButtonProps={{ danger: true }}>
|
||||
<Popconfirm
|
||||
title="确认删除?"
|
||||
onConfirm={() => handleDelete(record.deviceId)}
|
||||
okText="确认"
|
||||
cancelText="取消"
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Tooltip title="删除">
|
||||
<Button type="text" danger icon={<DeleteOutlined />} style={{ borderRadius: '6px' }} />
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
style={{ borderRadius: '6px' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
@@ -452,7 +503,9 @@ const IdleDeviceManagement = () => {
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', background: '#f8fafc', padding: '24px' }}>
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<Title level={4} style={{ marginBottom: '4px', color: '#1e293b' }}>空闲设备管理</Title>
|
||||
<Title level={4} style={{ marginBottom: '4px', color: '#1e293b' }}>
|
||||
空闲设备管理
|
||||
</Title>
|
||||
<Text type="secondary">管理已下线或空闲的设备,支持恢复领用到设备管理</Text>
|
||||
</div>
|
||||
|
||||
@@ -468,24 +521,38 @@ const IdleDeviceManagement = () => {
|
||||
}}
|
||||
bodyStyle={{ padding: '20px' }}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div
|
||||
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
|
||||
>
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: '13px' }}>{stat.title}</Text>
|
||||
<div style={{ fontSize: '28px', fontWeight: 700, color: stat.color, lineHeight: 1.2, marginTop: '4px' }}>
|
||||
<Text type="secondary" style={{ fontSize: '13px' }}>
|
||||
{stat.title}
|
||||
</Text>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '28px',
|
||||
fontWeight: 700,
|
||||
color: stat.color,
|
||||
lineHeight: 1.2,
|
||||
marginTop: '4px',
|
||||
}}
|
||||
>
|
||||
{stat.value}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
borderRadius: '12px',
|
||||
background: 'rgba(255,255,255,0.7)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '20px',
|
||||
color: stat.color
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
borderRadius: '12px',
|
||||
background: 'rgba(255,255,255,0.7)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '20px',
|
||||
color: stat.color,
|
||||
}}
|
||||
>
|
||||
{stat.icon}
|
||||
</div>
|
||||
</div>
|
||||
@@ -502,12 +569,14 @@ const IdleDeviceManagement = () => {
|
||||
}}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<div style={{
|
||||
padding: '20px 24px',
|
||||
borderBottom: '1px solid #f1f5f9',
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '16px 16px 0 0',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
padding: '20px 24px',
|
||||
borderBottom: '1px solid #f1f5f9',
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '16px 16px 0 0',
|
||||
}}
|
||||
>
|
||||
<Row gutter={16} align="middle">
|
||||
<Col flex="auto">
|
||||
<Space size="middle" wrap>
|
||||
@@ -516,7 +585,7 @@ const IdleDeviceManagement = () => {
|
||||
prefix={<SearchOutlined style={{ color: '#94a3b8' }} />}
|
||||
style={{ borderRadius: '10px', width: '260px', height: '40px' }}
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
onChange={e => setSearchKeyword(e.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
<Select
|
||||
@@ -528,7 +597,11 @@ const IdleDeviceManagement = () => {
|
||||
<Option value="rack">机架</Option>
|
||||
<Option value="warehouse">库房</Option>
|
||||
</Select>
|
||||
<Button icon={<ReloadOutlined />} onClick={fetchIdleDevices} style={{ height: 40, borderRadius: '10px' }}>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={fetchIdleDevices}
|
||||
style={{ height: 40, borderRadius: '10px' }}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
@@ -561,12 +634,14 @@ const IdleDeviceManagement = () => {
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
scroll={{ x: 1100 }}
|
||||
rowClassName={(record, index) => index % 2 === 0 ? 'table-row-even' : 'table-row-odd'}
|
||||
rowClassName={(record, index) => (index % 2 === 0 ? 'table-row-even' : 'table-row-odd')}
|
||||
style={{ borderRadius: '0 0 16px 16px' }}
|
||||
/>
|
||||
|
||||
{pagination.total > 0 && (
|
||||
<div style={{ padding: '16px 24px', borderTop: '1px solid #f1f5f9', background: '#fafafa' }}>
|
||||
<div
|
||||
style={{ padding: '16px 24px', borderTop: '1px solid #f1f5f9', background: '#fafafa' }}
|
||||
>
|
||||
<Row justify="space-between" align="middle">
|
||||
<Col>
|
||||
<Text type="secondary">
|
||||
@@ -579,11 +654,11 @@ const IdleDeviceManagement = () => {
|
||||
pageSize={pagination.pageSize}
|
||||
total={pagination.total}
|
||||
onChange={(page, pageSize) =>
|
||||
setPagination((prev) => ({ ...prev, current: page, pageSize }))
|
||||
setPagination(prev => ({ ...prev, current: page, pageSize }))
|
||||
}
|
||||
showSizeChanger
|
||||
showQuickJumper
|
||||
showTotal={(total) => `共 ${total} 条`}
|
||||
showTotal={total => `共 ${total} 条`}
|
||||
size="small"
|
||||
/>
|
||||
</Col>
|
||||
@@ -595,12 +670,14 @@ const IdleDeviceManagement = () => {
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<div style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: editingDevice ? '#3b82f6' : '#f59e0b'
|
||||
}} />
|
||||
<div
|
||||
style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: editingDevice ? '#3b82f6' : '#f59e0b',
|
||||
}}
|
||||
/>
|
||||
{editingDevice ? '编辑空闲设备' : '添加空闲设备'}
|
||||
</div>
|
||||
}
|
||||
@@ -615,28 +692,38 @@ const IdleDeviceManagement = () => {
|
||||
style={{ top: 100 }}
|
||||
>
|
||||
<Form form={form} layout="vertical" size="middle">
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
marginBottom: '16px',
|
||||
border: '1px solid #e2e8f0'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<div style={{ width: '4px', height: '16px', background: '#3b82f6', borderRadius: '2px' }} />
|
||||
border: '1px solid #e2e8f0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ width: '4px', height: '16px', background: '#3b82f6', borderRadius: '2px' }}
|
||||
/>
|
||||
设备基本信息
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="name" label="设备名称" rules={[{ required: true, message: '请输入设备名称' }]}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="设备名称"
|
||||
rules={[{ required: true, message: '请输入设备名称' }]}
|
||||
>
|
||||
<Input placeholder="请输入设备名称" style={{ borderRadius: '8px' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
@@ -667,36 +754,51 @@ const IdleDeviceManagement = () => {
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="powerConsumption" label="功耗(W)">
|
||||
<Input type="number" placeholder="请输入功耗" min={0} style={{ borderRadius: '8px' }} />
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入功耗"
|
||||
min={0}
|
||||
style={{ borderRadius: '8px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{editingDevice && (
|
||||
<Col span={12}>
|
||||
<Form.Item label="设备ID">
|
||||
<Input value={editingDevice.deviceId} disabled style={{ borderRadius: '8px' }} />
|
||||
<Input
|
||||
value={editingDevice.deviceId}
|
||||
disabled
|
||||
style={{ borderRadius: '8px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
marginBottom: '16px',
|
||||
border: '1px solid #e2e8f0'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<div style={{ width: '4px', height: '16px', background: '#22c55e', borderRadius: '2px' }} />
|
||||
border: '1px solid #e2e8f0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ width: '4px', height: '16px', background: '#22c55e', borderRadius: '2px' }}
|
||||
/>
|
||||
位置信息
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
@@ -705,7 +807,7 @@ const IdleDeviceManagement = () => {
|
||||
<Select
|
||||
placeholder="请选择机房"
|
||||
allowClear
|
||||
onChange={(value) => {
|
||||
onChange={value => {
|
||||
setSelectedRoomId(value);
|
||||
form.setFieldsValue({ rackId: null, position: null });
|
||||
if (value) {
|
||||
@@ -714,7 +816,7 @@ const IdleDeviceManagement = () => {
|
||||
}}
|
||||
style={{ borderRadius: '8px' }}
|
||||
>
|
||||
{rooms.map((room) => (
|
||||
{rooms.map(room => (
|
||||
<Option key={room.roomId} value={room.roomId}>
|
||||
{room.name}
|
||||
</Option>
|
||||
@@ -725,14 +827,14 @@ const IdleDeviceManagement = () => {
|
||||
<Col span={8}>
|
||||
<Form.Item name="rackId" label="机柜">
|
||||
<Select
|
||||
placeholder={selectedRoomId ? "请选择机柜" : "请先选择机房"}
|
||||
placeholder={selectedRoomId ? '请选择机柜' : '请先选择机房'}
|
||||
allowClear
|
||||
disabled={!selectedRoomId}
|
||||
style={{ borderRadius: '8px' }}
|
||||
>
|
||||
{racks
|
||||
.filter((rack) => rack.roomId === selectedRoomId)
|
||||
.map((rack) => (
|
||||
.filter(rack => rack.roomId === selectedRoomId)
|
||||
.map(rack => (
|
||||
<Option key={rack.rackId} value={rack.rackId}>
|
||||
{rack.name}
|
||||
</Option>
|
||||
@@ -742,11 +844,19 @@ const IdleDeviceManagement = () => {
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="position" label="U位">
|
||||
<Input type="number" placeholder="请输入U位" min={1} disabled={!selectedRoomId} style={{ borderRadius: '8px' }} />
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入U位"
|
||||
min={1}
|
||||
disabled={!selectedRoomId}
|
||||
style={{ borderRadius: '8px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<div style={{ textAlign: 'center', color: '#94a3b8', fontSize: '12px', margin: '8px 0' }}>
|
||||
<div
|
||||
style={{ textAlign: 'center', color: '#94a3b8', fontSize: '12px', margin: '8px 0' }}
|
||||
>
|
||||
— 或 —
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
@@ -769,28 +879,37 @@ const IdleDeviceManagement = () => {
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
border: '1px solid #e2e8f0'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<div style={{ width: '4px', height: '16px', background: '#64748b', borderRadius: '2px' }} />
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
border: '1px solid #e2e8f0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ width: '4px', height: '16px', background: '#64748b', borderRadius: '2px' }}
|
||||
/>
|
||||
附加信息
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Form.Item name="idleReason" label="空闲原因">
|
||||
<Input placeholder="请输入空闲原因,如:设备下线、备件库存等" style={{ borderRadius: '8px' }} />
|
||||
<Input
|
||||
placeholder="请输入空闲原因,如:设备下线、备件库存等"
|
||||
style={{ borderRadius: '8px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -808,7 +927,9 @@ const IdleDeviceManagement = () => {
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<div style={{ width: '8px', height: '8px', borderRadius: '50%', background: '#22c55e' }} />
|
||||
<div
|
||||
style={{ width: '8px', height: '8px', borderRadius: '50%', background: '#22c55e' }}
|
||||
/>
|
||||
设备上架
|
||||
</div>
|
||||
}
|
||||
@@ -822,33 +943,47 @@ const IdleDeviceManagement = () => {
|
||||
bodyStyle={{ padding: '24px' }}
|
||||
>
|
||||
<Form form={shelveForm} layout="vertical" size="middle">
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
marginBottom: '16px',
|
||||
border: '1px solid #e2e8f0'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<div style={{ width: '4px', height: '16px', background: '#3b82f6', borderRadius: '2px' }} />
|
||||
border: '1px solid #e2e8f0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ width: '4px', height: '16px', background: '#3b82f6', borderRadius: '2px' }}
|
||||
/>
|
||||
设备基本信息
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="name" label="设备名称" rules={[{ required: true, message: '请输入设备名称' }]}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="设备名称"
|
||||
rules={[{ required: true, message: '请输入设备名称' }]}
|
||||
>
|
||||
<Input placeholder="请输入设备名称" style={{ borderRadius: '8px' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="type" label="设备类型" rules={[{ required: true, message: '请选择设备类型' }]}>
|
||||
<Form.Item
|
||||
name="type"
|
||||
label="设备类型"
|
||||
rules={[{ required: true, message: '请选择设备类型' }]}
|
||||
>
|
||||
<Select placeholder="请选择设备类型" style={{ borderRadius: '8px' }}>
|
||||
<Option value="server">服务器</Option>
|
||||
<Option value="switch">交换机</Option>
|
||||
@@ -866,7 +1001,11 @@ const IdleDeviceManagement = () => {
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="serialNumber" label="序列号" rules={[{ required: true, message: '请输入序列号' }]}>
|
||||
<Form.Item
|
||||
name="serialNumber"
|
||||
label="序列号"
|
||||
rules={[{ required: true, message: '请输入序列号' }]}
|
||||
>
|
||||
<Input placeholder="请输入序列号" style={{ borderRadius: '8px' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
@@ -874,34 +1013,50 @@ const IdleDeviceManagement = () => {
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="height" label="高度(U)">
|
||||
<Input type="number" placeholder="请输入高度" min={1} style={{ borderRadius: '8px' }} />
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入高度"
|
||||
min={1}
|
||||
style={{ borderRadius: '8px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="powerConsumption" label="功率(W)">
|
||||
<Input type="number" placeholder="请输入功率" min={0} style={{ borderRadius: '8px' }} />
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入功率"
|
||||
min={0}
|
||||
style={{ borderRadius: '8px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
marginBottom: '16px',
|
||||
border: '1px solid #e2e8f0'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<div style={{ width: '4px', height: '16px', background: '#22c55e', borderRadius: '2px' }} />
|
||||
border: '1px solid #e2e8f0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ width: '4px', height: '16px', background: '#22c55e', borderRadius: '2px' }}
|
||||
/>
|
||||
上架位置
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
@@ -909,13 +1064,13 @@ const IdleDeviceManagement = () => {
|
||||
<Form.Item name="roomId" label="机房">
|
||||
<Select
|
||||
placeholder="请选择机房"
|
||||
onChange={(value) => {
|
||||
onChange={value => {
|
||||
setSelectedShelveRoomId(value);
|
||||
shelveForm.setFieldsValue({ rackId: null });
|
||||
}}
|
||||
style={{ borderRadius: '8px' }}
|
||||
>
|
||||
{rooms.map((room) => (
|
||||
{rooms.map(room => (
|
||||
<Option key={room.roomId} value={room.roomId}>
|
||||
{room.name}
|
||||
</Option>
|
||||
@@ -926,14 +1081,14 @@ const IdleDeviceManagement = () => {
|
||||
<Col span={8}>
|
||||
<Form.Item name="rackId" label="机柜">
|
||||
<Select
|
||||
placeholder={selectedShelveRoomId ? "请选择机柜" : "请先选择机房"}
|
||||
placeholder={selectedShelveRoomId ? '请选择机柜' : '请先选择机房'}
|
||||
disabled={!selectedShelveRoomId}
|
||||
style={{ borderRadius: '8px' }}
|
||||
onChange={handleShelveRackChange}
|
||||
>
|
||||
{racks
|
||||
.filter((rack) => rack.roomId === selectedShelveRoomId)
|
||||
.map((rack) => (
|
||||
.filter(rack => rack.roomId === selectedShelveRoomId)
|
||||
.map(rack => (
|
||||
<Option key={rack.rackId} value={rack.rackId}>
|
||||
{rack.name}
|
||||
</Option>
|
||||
@@ -943,47 +1098,67 @@ const IdleDeviceManagement = () => {
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="position" label="U位">
|
||||
<Input type="number" placeholder="请输入U位" min={1} style={{ borderRadius: '8px' }} onChange={handleShelvePositionChange} />
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入U位"
|
||||
min={1}
|
||||
style={{ borderRadius: '8px' }}
|
||||
onChange={handleShelvePositionChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
{shelvePositionConflict && (
|
||||
<div style={{ marginTop: '12px' }}>
|
||||
<div style={{
|
||||
background: '#fef2f2',
|
||||
border: '1px solid #fecaca',
|
||||
borderRadius: '8px',
|
||||
padding: '12px 16px',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '10px'
|
||||
}}>
|
||||
<ExclamationCircleOutlined style={{ color: '#ef4444', fontSize: '18px', marginTop: '2px' }} />
|
||||
<div
|
||||
style={{
|
||||
background: '#fef2f2',
|
||||
border: '1px solid #fecaca',
|
||||
borderRadius: '8px',
|
||||
padding: '12px 16px',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '10px',
|
||||
}}
|
||||
>
|
||||
<ExclamationCircleOutlined
|
||||
style={{ color: '#ef4444', fontSize: '18px', marginTop: '2px' }}
|
||||
/>
|
||||
<div>
|
||||
<div style={{ color: '#dc2626', fontWeight: 600, marginBottom: '4px' }}>U位冲突</div>
|
||||
<div style={{ color: '#991b1b', fontSize: '13px' }}>{shelvePositionConflict}</div>
|
||||
<div style={{ color: '#dc2626', fontWeight: 600, marginBottom: '4px' }}>
|
||||
U位冲突
|
||||
</div>
|
||||
<div style={{ color: '#991b1b', fontSize: '13px' }}>
|
||||
{shelvePositionConflict}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
border: '1px solid #e2e8f0'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<div style={{ width: '4px', height: '16px', background: '#64748b', borderRadius: '2px' }} />
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
border: '1px solid #e2e8f0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ width: '4px', height: '16px', background: '#64748b', borderRadius: '2px' }}
|
||||
/>
|
||||
备注信息
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
@@ -1022,4 +1197,4 @@ const IdleDeviceManagement = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IdleDeviceManagement;
|
||||
export default IdleDeviceManagement;
|
||||
|
||||
@@ -48,7 +48,7 @@ const api = axios.create({
|
||||
baseURL: '/api',
|
||||
});
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
api.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
@@ -92,7 +92,7 @@ const InventoryManagement = () => {
|
||||
};
|
||||
const res = await api.get('/inventory/plans', { params });
|
||||
setPlans(res.data.plans || []);
|
||||
setPagination((prev) => ({
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
total: res.data.total || 0,
|
||||
}));
|
||||
@@ -119,7 +119,7 @@ const InventoryManagement = () => {
|
||||
const fetchRooms = async () => {
|
||||
try {
|
||||
const res = await api.get('/rooms', { params: { pageSize: 1000 } });
|
||||
setRooms(Array.isArray(res.data) ? res.data : (res.data.rooms || []));
|
||||
setRooms(Array.isArray(res.data) ? res.data : res.data.rooms || []);
|
||||
} catch (error) {
|
||||
console.error('获取机房失败', error);
|
||||
}
|
||||
@@ -128,7 +128,7 @@ const InventoryManagement = () => {
|
||||
const fetchRacks = async () => {
|
||||
try {
|
||||
const res = await api.get('/racks', { params: { pageSize: 1000 } });
|
||||
const allRacks = Array.isArray(res.data) ? res.data : (res.data.racks || []);
|
||||
const allRacks = Array.isArray(res.data) ? res.data : res.data.racks || [];
|
||||
setRacks(allRacks);
|
||||
setFilteredRacks(allRacks);
|
||||
} catch (error) {
|
||||
@@ -157,20 +157,21 @@ const InventoryManagement = () => {
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (record) => {
|
||||
const handleEdit = record => {
|
||||
setEditingPlan(record);
|
||||
const targetRooms = record.targetRooms || [];
|
||||
setSelectedRooms(targetRooms);
|
||||
|
||||
|
||||
if (targetRooms.length > 0) {
|
||||
const filtered = racks.filter(rack =>
|
||||
targetRooms.includes(rack.roomId) || (rack.Room && targetRooms.includes(rack.Room.roomId))
|
||||
const filtered = racks.filter(
|
||||
rack =>
|
||||
targetRooms.includes(rack.roomId) || (rack.Room && targetRooms.includes(rack.Room.roomId))
|
||||
);
|
||||
setFilteredRacks(filtered);
|
||||
} else {
|
||||
setFilteredRacks(racks);
|
||||
}
|
||||
|
||||
|
||||
form.setFieldsValue({
|
||||
name: record.name,
|
||||
type: record.type,
|
||||
@@ -182,7 +183,7 @@ const InventoryManagement = () => {
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (planId) => {
|
||||
const handleDelete = async planId => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个盘点计划吗?此操作不可恢复!',
|
||||
@@ -202,7 +203,7 @@ const InventoryManagement = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (values) => {
|
||||
const handleSubmit = async values => {
|
||||
try {
|
||||
const data = {
|
||||
...values,
|
||||
@@ -226,7 +227,7 @@ const InventoryManagement = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleStart = async (plan) => {
|
||||
const handleStart = async plan => {
|
||||
try {
|
||||
await api.post(`/inventory/plans/${plan.planId}/start`);
|
||||
message.success('盘点任务已启动');
|
||||
@@ -237,7 +238,7 @@ const InventoryManagement = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleComplete = async (plan) => {
|
||||
const handleComplete = async plan => {
|
||||
try {
|
||||
await api.post(`/inventory/plans/${plan.planId}/complete`);
|
||||
message.success('盘点已完成');
|
||||
@@ -248,23 +249,23 @@ const InventoryManagement = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewTasks = (plan) => {
|
||||
const handleViewTasks = plan => {
|
||||
navigate(`/inventory/execution?planId=${plan.planId}`);
|
||||
};
|
||||
|
||||
const handleRoomsChange = (roomIds) => {
|
||||
const handleRoomsChange = roomIds => {
|
||||
setSelectedRooms(roomIds || []);
|
||||
if (!roomIds || roomIds.length === 0) {
|
||||
setFilteredRacks(racks);
|
||||
} else {
|
||||
const filtered = racks.filter(rack =>
|
||||
roomIds.includes(rack.roomId) || (rack.Room && roomIds.includes(rack.Room.roomId))
|
||||
const filtered = racks.filter(
|
||||
rack => roomIds.includes(rack.roomId) || (rack.Room && roomIds.includes(rack.Room.roomId))
|
||||
);
|
||||
setFilteredRacks(filtered);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusTag = (status) => {
|
||||
const getStatusTag = status => {
|
||||
const statusMap = {
|
||||
draft: { color: 'default', text: '草稿', icon: <FileSearchOutlined /> },
|
||||
pending: { color: 'orange', text: '待执行', icon: <ClockCircleOutlined /> },
|
||||
@@ -274,17 +275,13 @@ const InventoryManagement = () => {
|
||||
};
|
||||
const config = statusMap[status] || statusMap.draft;
|
||||
return (
|
||||
<Tag
|
||||
color={config.color}
|
||||
icon={config.icon}
|
||||
style={{ borderRadius: 6, padding: '2px 8px' }}
|
||||
>
|
||||
<Tag color={config.color} icon={config.icon} style={{ borderRadius: 6, padding: '2px 8px' }}>
|
||||
{config.text}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
const getTypeTag = (type) => {
|
||||
const getTypeTag = type => {
|
||||
const typeMap = {
|
||||
full: { color: 'blue', text: '全面盘点' },
|
||||
partial: { color: 'cyan', text: '局部盘点' },
|
||||
@@ -294,7 +291,7 @@ const InventoryManagement = () => {
|
||||
return <Tag color={config.color}>{config.text}</Tag>;
|
||||
};
|
||||
|
||||
const getProgressPercent = (plan) => {
|
||||
const getProgressPercent = plan => {
|
||||
if (!plan.totalDevices || plan.totalDevices === 0) return 0;
|
||||
return Math.round((plan.checkedDevices / plan.totalDevices) * 100);
|
||||
};
|
||||
@@ -334,14 +331,14 @@ const InventoryManagement = () => {
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 100,
|
||||
render: (type) => getTypeTag(type),
|
||||
render: type => getTypeTag(type),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 120,
|
||||
render: (status) => getStatusTag(status),
|
||||
render: status => getStatusTag(status),
|
||||
},
|
||||
{
|
||||
title: '盘点进度',
|
||||
@@ -357,8 +354,8 @@ const InventoryManagement = () => {
|
||||
{getProgressPercent(record)}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
percent={getProgressPercent(record)}
|
||||
<Progress
|
||||
percent={getProgressPercent(record)}
|
||||
size="small"
|
||||
strokeColor={{
|
||||
'0%': '#108ee9',
|
||||
@@ -372,25 +369,26 @@ const InventoryManagement = () => {
|
||||
title: '异常设备',
|
||||
key: 'abnormal',
|
||||
width: 100,
|
||||
render: (_, record) => (
|
||||
record.abnormalDevices > 0 ?
|
||||
<Tag color="error">{record.abnormalDevices} 异常</Tag> :
|
||||
render: (_, record) =>
|
||||
record.abnormalDevices > 0 ? (
|
||||
<Tag color="error">{record.abnormalDevices} 异常</Tag>
|
||||
) : (
|
||||
<span style={{ color: '#8c8c8c' }}>-</span>
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
dataIndex: ['Creator', 'realName'],
|
||||
key: 'creator',
|
||||
width: 100,
|
||||
render: (name) => name || '-',
|
||||
render: name => name || '-',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 160,
|
||||
render: (date) => (date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '-'),
|
||||
render: date => (date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '-'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
@@ -482,21 +480,29 @@ const InventoryManagement = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24, background: designTokens.colors.background.secondary, minHeight: '100vh' }}>
|
||||
<div
|
||||
style={{
|
||||
padding: 24,
|
||||
background: designTokens.colors.background.secondary,
|
||||
minHeight: '100vh',
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<Row gutter={[16, 16]}>
|
||||
{statCards.map((stat, index) => (
|
||||
<Col xs={24} sm={8} key={index}>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
|
||||
background: stat.gradient,
|
||||
}}
|
||||
bodyStyle={{ padding: 20 }}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div
|
||||
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}
|
||||
>
|
||||
<div>
|
||||
<div style={{ color: 'rgba(255,255,255,0.9)', fontSize: 14, marginBottom: 8 }}>
|
||||
{stat.title}
|
||||
@@ -505,17 +511,19 @@ const InventoryManagement = () => {
|
||||
{stat.value || 0}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 12,
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 24,
|
||||
color: '#fff'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 12,
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 24,
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
{stat.icon}
|
||||
</div>
|
||||
</div>
|
||||
@@ -525,25 +533,27 @@ const InventoryManagement = () => {
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{ borderRadius: 16, boxShadow: '0 2px 8px rgba(0,0,0,0.06)' }}
|
||||
>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<Card bordered={false} style={{ borderRadius: 16, boxShadow: '0 2px 8px rgba(0,0,0,0.06)' }}>
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab} style={{ marginBottom: 16 }}>
|
||||
<TabPane tab="盘点计划列表" key="list">
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Input
|
||||
placeholder="搜索计划名称"
|
||||
prefix={<SearchOutlined />}
|
||||
style={{ width: 240, borderRadius: 8 }}
|
||||
onChange={(e) => setSearchParamsObj({ keyword: e.target.value })}
|
||||
onChange={e => setSearchParamsObj({ keyword: e.target.value })}
|
||||
onPressEnter={() => {
|
||||
setPagination((prev) => ({ ...prev, current: 1 }));
|
||||
setPagination(prev => ({ ...prev, current: 1 }));
|
||||
fetchPlans();
|
||||
}}
|
||||
/>
|
||||
@@ -551,9 +561,9 @@ const InventoryManagement = () => {
|
||||
placeholder="选择状态"
|
||||
style={{ width: 140, borderRadius: 8 }}
|
||||
allowClear
|
||||
onChange={(value) => {
|
||||
setSearchParamsObj((prev) => ({ ...prev, status: value }));
|
||||
setPagination((prev) => ({ ...prev, current: 1 }));
|
||||
onChange={value => {
|
||||
setSearchParamsObj(prev => ({ ...prev, status: value }));
|
||||
setPagination(prev => ({ ...prev, current: 1 }));
|
||||
}}
|
||||
>
|
||||
<Select.Option value="draft">草稿</Select.Option>
|
||||
@@ -561,17 +571,21 @@ const InventoryManagement = () => {
|
||||
<Select.Option value="in_progress">进行中</Select.Option>
|
||||
<Select.Option value="completed">已完成</Select.Option>
|
||||
</Select>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => { setSearchParamsObj({}); fetchPlans(); fetchStats(); }}
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => {
|
||||
setSearchParamsObj({});
|
||||
fetchPlans();
|
||||
fetchStats();
|
||||
}}
|
||||
style={{ borderRadius: 8 }}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleAdd}
|
||||
style={{ borderRadius: 8, height: 40 }}
|
||||
>
|
||||
@@ -588,14 +602,16 @@ const InventoryManagement = () => {
|
||||
...pagination,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`,
|
||||
showTotal: total => `共 ${total} 条记录`,
|
||||
onChange: (page, pageSize) => {
|
||||
setPagination((prev) => ({ ...prev, current: page, pageSize }));
|
||||
setPagination(prev => ({ ...prev, current: page, pageSize }));
|
||||
},
|
||||
}}
|
||||
scroll={{ x: 1200 }}
|
||||
locale={{
|
||||
emptyText: <Empty description="暂无盘点计划" image={Empty.PRESENTED_IMAGE_SIMPLE} />,
|
||||
emptyText: (
|
||||
<Empty description="暂无盘点计划" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</TabPane>
|
||||
@@ -615,17 +631,13 @@ const InventoryManagement = () => {
|
||||
footer={null}
|
||||
width={640}
|
||||
centered
|
||||
styles={{
|
||||
styles={{
|
||||
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' },
|
||||
body: { padding: 24 },
|
||||
footer: { borderTop: '1px solid #f0f0f0', padding: '12px 24px' }
|
||||
footer: { borderTop: '1px solid #f0f0f0', padding: '12px 24px' },
|
||||
}}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>
|
||||
<Form.Item
|
||||
@@ -652,35 +664,26 @@ const InventoryManagement = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="描述"
|
||||
>
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea rows={2} placeholder="请输入描述" style={{ borderRadius: 8 }} />
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="scheduledDate"
|
||||
label="计划日期"
|
||||
>
|
||||
<Form.Item name="scheduledDate" label="计划日期">
|
||||
<DatePicker style={{ width: '100%', borderRadius: 8 }} placeholder="选择计划日期" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="targetRooms"
|
||||
label="目标机房"
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="选择目标机房(不选则为全部)"
|
||||
<Form.Item name="targetRooms" label="目标机房">
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="选择目标机房(不选则为全部)"
|
||||
allowClear
|
||||
onChange={handleRoomsChange}
|
||||
style={{ borderRadius: 8 }}
|
||||
>
|
||||
{rooms.map((room) => (
|
||||
{rooms.map(room => (
|
||||
<Select.Option key={room.roomId} value={room.roomId}>
|
||||
{room.name}
|
||||
</Select.Option>
|
||||
@@ -690,17 +693,14 @@ const InventoryManagement = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
name="targetRacks"
|
||||
label="目标机柜"
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="选择目标机柜(不选则为全部)"
|
||||
<Form.Item name="targetRacks" label="目标机柜">
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="选择目标机柜(不选则为全部)"
|
||||
allowClear
|
||||
style={{ borderRadius: 8 }}
|
||||
>
|
||||
{filteredRacks.map((rack) => (
|
||||
{filteredRacks.map(rack => (
|
||||
<Select.Option key={rack.rackId} value={rack.rackId}>
|
||||
{rack.name} ({rack.Room?.name || ''})
|
||||
</Select.Option>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -374,9 +374,7 @@ const Login = () => {
|
||||
>
|
||||
{headerContent.title}
|
||||
</Title>
|
||||
<Text style={{ fontSize: '15px', color: '#64748b' }}>
|
||||
{headerContent.subtitle}
|
||||
</Text>
|
||||
<Text style={{ fontSize: '15px', color: '#64748b' }}>{headerContent.subtitle}</Text>
|
||||
</div>
|
||||
|
||||
{/* 首次使用提示 */}
|
||||
@@ -436,7 +434,11 @@ const Login = () => {
|
||||
rules={[{ required: true, message: '请输入真实姓名' }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<SafetyCertificateOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
|
||||
prefix={
|
||||
<SafetyCertificateOutlined
|
||||
style={{ color: '#94a3b8', fontSize: '18px' }}
|
||||
/>
|
||||
}
|
||||
placeholder="请输入真实姓名"
|
||||
style={{ borderRadius: '12px', height: '48px' }}
|
||||
/>
|
||||
|
||||
@@ -27,7 +27,11 @@ import {
|
||||
import api from '../api';
|
||||
import CloseButton from '../components/CloseButton';
|
||||
import dayjs from 'dayjs';
|
||||
import { selectStyles, filterInputStyles, inputPlaceholders } from '../styles/deviceManagementStyles';
|
||||
import {
|
||||
selectStyles,
|
||||
filterInputStyles,
|
||||
inputPlaceholders,
|
||||
} from '../styles/deviceManagementStyles';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
const { Option } = Select;
|
||||
@@ -98,7 +102,7 @@ function OperationLogs() {
|
||||
...prev,
|
||||
current: page,
|
||||
pageSize,
|
||||
total: response.data.total
|
||||
total: response.data.total,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -135,7 +139,7 @@ function OperationLogs() {
|
||||
fetchLogs(1, pagination.pageSize, clearedFilters);
|
||||
};
|
||||
|
||||
const handleViewDetail = async (record) => {
|
||||
const handleViewDetail = async record => {
|
||||
setDetailLoading(true);
|
||||
setDetailVisible(true);
|
||||
try {
|
||||
@@ -151,7 +155,7 @@ function OperationLogs() {
|
||||
}
|
||||
};
|
||||
|
||||
const getModuleTag = (module) => {
|
||||
const getModuleTag = module => {
|
||||
const colors = {
|
||||
device: 'blue',
|
||||
user: 'green',
|
||||
@@ -175,7 +179,7 @@ function OperationLogs() {
|
||||
return <Tag color={colors[module] || 'default'}>{names[module] || module}</Tag>;
|
||||
};
|
||||
|
||||
const getOperationTag = (type) => {
|
||||
const getOperationTag = type => {
|
||||
const colors = {
|
||||
create: 'green',
|
||||
update: 'blue',
|
||||
@@ -203,10 +207,8 @@ function OperationLogs() {
|
||||
return <Tag color={colors[type] || 'default'}>{names[type] || type}</Tag>;
|
||||
};
|
||||
|
||||
const getResultTag = (result) => {
|
||||
return result === 'success'
|
||||
? <Tag color="success">成功</Tag>
|
||||
: <Tag color="error">失败</Tag>;
|
||||
const getResultTag = result => {
|
||||
return result === 'success' ? <Tag color="success">成功</Tag> : <Tag color="error">失败</Tag>;
|
||||
};
|
||||
|
||||
const columns = [
|
||||
@@ -272,18 +274,19 @@ function OperationLogs() {
|
||||
width: 80,
|
||||
fixed: 'right',
|
||||
render: (_, record) => (
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleViewDetail(record)}
|
||||
>
|
||||
<Button type="link" icon={<EyeOutlined />} onClick={() => handleViewDetail(record)}>
|
||||
详情
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const hasFilters = filters.module || filters.operationType || filters.keyword || filters.dateRange || filters.result;
|
||||
const hasFilters =
|
||||
filters.module ||
|
||||
filters.operationType ||
|
||||
filters.keyword ||
|
||||
filters.dateRange ||
|
||||
filters.result;
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
@@ -311,7 +314,9 @@ function OperationLogs() {
|
||||
onChange={value => handleFilterChange('module', value)}
|
||||
>
|
||||
{MODULE_OPTIONS.map(opt => (
|
||||
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
|
||||
<Option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Col>
|
||||
@@ -324,7 +329,9 @@ function OperationLogs() {
|
||||
onChange={value => handleFilterChange('operationType', value)}
|
||||
>
|
||||
{OPERATION_TYPE_OPTIONS.map(opt => (
|
||||
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
|
||||
<Option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Col>
|
||||
@@ -337,7 +344,9 @@ function OperationLogs() {
|
||||
onChange={value => handleFilterChange('result', value)}
|
||||
>
|
||||
{RESULT_OPTIONS.map(opt => (
|
||||
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
|
||||
<Option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Col>
|
||||
@@ -368,10 +377,7 @@ function OperationLogs() {
|
||||
筛选
|
||||
</Button>
|
||||
{hasFilters && (
|
||||
<Button
|
||||
icon={<ClearOutlined />}
|
||||
onClick={handleClearFilters}
|
||||
>
|
||||
<Button icon={<ClearOutlined />} onClick={handleClearFilters}>
|
||||
清除
|
||||
</Button>
|
||||
)}
|
||||
@@ -391,7 +397,7 @@ function OperationLogs() {
|
||||
total: pagination.total,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
showTotal: total => `共 ${total} 条`,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
}}
|
||||
onChange={handleTableChange}
|
||||
@@ -428,7 +434,9 @@ function OperationLogs() {
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="模块">{currentLog.module}</Descriptions.Item>
|
||||
<Descriptions.Item label="操作类型">{currentLog.operationType}</Descriptions.Item>
|
||||
<Descriptions.Item label="操作描述">{currentLog.operationDescription}</Descriptions.Item>
|
||||
<Descriptions.Item label="操作描述">
|
||||
{currentLog.operationDescription}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="目标ID">{currentLog.targetId || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="目标名称">{currentLog.targetName || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="操作人ID">{currentLog.operatorId}</Descriptions.Item>
|
||||
@@ -450,7 +458,15 @@ function OperationLogs() {
|
||||
{currentLog.beforeState && (
|
||||
<>
|
||||
<Text strong>变更前:</Text>
|
||||
<pre style={{ background: '#f5f5f5', padding: 12, borderRadius: 4, overflow: 'auto', maxHeight: 200 }}>
|
||||
<pre
|
||||
style={{
|
||||
background: '#f5f5f5',
|
||||
padding: 12,
|
||||
borderRadius: 4,
|
||||
overflow: 'auto',
|
||||
maxHeight: 200,
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(currentLog.beforeState, null, 2)}
|
||||
</pre>
|
||||
</>
|
||||
@@ -458,7 +474,15 @@ function OperationLogs() {
|
||||
{currentLog.afterState && (
|
||||
<>
|
||||
<Text strong>变更后:</Text>
|
||||
<pre style={{ background: '#f0f0f0', padding: 12, borderRadius: 4, overflow: 'auto', maxHeight: 200 }}>
|
||||
<pre
|
||||
style={{
|
||||
background: '#f0f0f0',
|
||||
padding: 12,
|
||||
borderRadius: 4,
|
||||
overflow: 'auto',
|
||||
maxHeight: 200,
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(currentLog.afterState, null, 2)}
|
||||
</pre>
|
||||
</>
|
||||
@@ -469,7 +493,15 @@ function OperationLogs() {
|
||||
{currentLog && currentLog.metadata && Object.keys(currentLog.metadata).length > 0 && (
|
||||
<>
|
||||
<h4 style={{ marginTop: 16 }}>扩展信息</h4>
|
||||
<pre style={{ background: '#f5f5f5', padding: 12, borderRadius: 4, overflow: 'auto', maxHeight: 200 }}>
|
||||
<pre
|
||||
style={{
|
||||
background: '#f5f5f5',
|
||||
padding: 12,
|
||||
borderRadius: 4,
|
||||
overflow: 'auto',
|
||||
maxHeight: 200,
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(currentLog.metadata, null, 2)}
|
||||
</pre>
|
||||
</>
|
||||
|
||||
@@ -42,7 +42,7 @@ const api = axios.create({
|
||||
baseURL: '/api',
|
||||
});
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
api.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
@@ -64,13 +64,13 @@ const PendingDeviceManagement = () => {
|
||||
const [currentDevice, setCurrentDevice] = useState(null);
|
||||
const [form] = Form.useForm();
|
||||
const [selectedRoomId, setSelectedRoomId] = useState(null);
|
||||
|
||||
|
||||
const [pagination, setPagination] = useState({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
|
||||
const [filters, setFilters] = useState({
|
||||
status: null,
|
||||
planId: null,
|
||||
@@ -79,27 +79,133 @@ const PendingDeviceManagement = () => {
|
||||
});
|
||||
|
||||
const defaultDeviceFields = [
|
||||
{ fieldName: 'deviceName', displayName: '设备名称', fieldType: 'string', required: true, visible: true, order: 1 },
|
||||
{ fieldName: 'deviceType', displayName: '设备类型', fieldType: 'select', required: true, visible: true, order: 2, options: [
|
||||
{ value: 'server', label: '服务器' },
|
||||
{ value: 'switch', label: '交换机' },
|
||||
{ value: 'router', label: '路由器' },
|
||||
{ value: 'storage', label: '存储设备' },
|
||||
{ value: 'other', label: '其他' },
|
||||
]},
|
||||
{ fieldName: 'model', displayName: '型号', fieldType: 'string', required: false, visible: true, order: 3 },
|
||||
{ fieldName: 'serialNumber', displayName: '序列号', fieldType: 'string', required: true, visible: true, order: 4 },
|
||||
{ fieldName: 'roomId', displayName: '所属机房', fieldType: 'select', required: false, visible: true, order: 5 },
|
||||
{ fieldName: 'rackId', displayName: '所属机柜', fieldType: 'select', required: false, visible: true, order: 6 },
|
||||
{ fieldName: 'position', displayName: '位置(U)', fieldType: 'number', required: false, visible: true, order: 7 },
|
||||
{ fieldName: 'height', displayName: '高度(U)', fieldType: 'number', required: false, visible: true, order: 8 },
|
||||
{ fieldName: 'powerConsumption', displayName: '功率(W)', fieldType: 'number', required: false, visible: true, order: 9 },
|
||||
{ fieldName: 'purchaseDate', displayName: '购买日期', fieldType: 'date', required: false, visible: true, order: 10 },
|
||||
{ fieldName: 'warrantyExpiry', displayName: '保修到期', fieldType: 'date', required: false, visible: true, order: 11 },
|
||||
{ fieldName: 'ipAddress', displayName: 'IP地址', fieldType: 'string', required: false, visible: true, order: 12 },
|
||||
{ fieldName: 'brand', displayName: '品牌', fieldType: 'string', required: false, visible: true, order: 13 },
|
||||
{ fieldName: 'description', displayName: '描述', fieldType: 'textarea', required: false, visible: true, order: 14 },
|
||||
{ fieldName: 'remark', displayName: '备注', fieldType: 'textarea', required: false, visible: true, order: 15 },
|
||||
{
|
||||
fieldName: 'deviceName',
|
||||
displayName: '设备名称',
|
||||
fieldType: 'string',
|
||||
required: true,
|
||||
visible: true,
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
fieldName: 'deviceType',
|
||||
displayName: '设备类型',
|
||||
fieldType: 'select',
|
||||
required: true,
|
||||
visible: true,
|
||||
order: 2,
|
||||
options: [
|
||||
{ value: 'server', label: '服务器' },
|
||||
{ value: 'switch', label: '交换机' },
|
||||
{ value: 'router', label: '路由器' },
|
||||
{ value: 'storage', label: '存储设备' },
|
||||
{ value: 'other', label: '其他' },
|
||||
],
|
||||
},
|
||||
{
|
||||
fieldName: 'model',
|
||||
displayName: '型号',
|
||||
fieldType: 'string',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 3,
|
||||
},
|
||||
{
|
||||
fieldName: 'serialNumber',
|
||||
displayName: '序列号',
|
||||
fieldType: 'string',
|
||||
required: true,
|
||||
visible: true,
|
||||
order: 4,
|
||||
},
|
||||
{
|
||||
fieldName: 'roomId',
|
||||
displayName: '所属机房',
|
||||
fieldType: 'select',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 5,
|
||||
},
|
||||
{
|
||||
fieldName: 'rackId',
|
||||
displayName: '所属机柜',
|
||||
fieldType: 'select',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 6,
|
||||
},
|
||||
{
|
||||
fieldName: 'position',
|
||||
displayName: '位置(U)',
|
||||
fieldType: 'number',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 7,
|
||||
},
|
||||
{
|
||||
fieldName: 'height',
|
||||
displayName: '高度(U)',
|
||||
fieldType: 'number',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 8,
|
||||
},
|
||||
{
|
||||
fieldName: 'powerConsumption',
|
||||
displayName: '功率(W)',
|
||||
fieldType: 'number',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 9,
|
||||
},
|
||||
{
|
||||
fieldName: 'purchaseDate',
|
||||
displayName: '购买日期',
|
||||
fieldType: 'date',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 10,
|
||||
},
|
||||
{
|
||||
fieldName: 'warrantyExpiry',
|
||||
displayName: '保修到期',
|
||||
fieldType: 'date',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 11,
|
||||
},
|
||||
{
|
||||
fieldName: 'ipAddress',
|
||||
displayName: 'IP地址',
|
||||
fieldType: 'string',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 12,
|
||||
},
|
||||
{
|
||||
fieldName: 'brand',
|
||||
displayName: '品牌',
|
||||
fieldType: 'string',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 13,
|
||||
},
|
||||
{
|
||||
fieldName: 'description',
|
||||
displayName: '描述',
|
||||
fieldType: 'textarea',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 14,
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
displayName: '备注',
|
||||
fieldType: 'textarea',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 15,
|
||||
},
|
||||
];
|
||||
|
||||
const fetchDeviceFields = async () => {
|
||||
@@ -185,11 +291,11 @@ const PendingDeviceManagement = () => {
|
||||
fetchDeviceFields();
|
||||
}, []);
|
||||
|
||||
const filteredRacks = selectedRoomId
|
||||
const filteredRacks = selectedRoomId
|
||||
? racks.filter(rack => rack.roomId === selectedRoomId)
|
||||
: racks;
|
||||
|
||||
const handleSync = async (pendingId) => {
|
||||
const handleSync = async pendingId => {
|
||||
try {
|
||||
const res = await api.post(`/inventory/pending-devices/${pendingId}/sync`);
|
||||
message.success(res.data.message);
|
||||
@@ -205,7 +311,7 @@ const PendingDeviceManagement = () => {
|
||||
message.warning('请先选择要同步的设备');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const res = await api.post('/inventory/pending-devices/batch-sync', {
|
||||
pendingIds: selectedRowKeys,
|
||||
@@ -219,27 +325,27 @@ const PendingDeviceManagement = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (record) => {
|
||||
const handleEdit = record => {
|
||||
setCurrentDevice(record);
|
||||
setSelectedRoomId(record.roomId);
|
||||
|
||||
|
||||
// 字段名映射:数据库字段名 -> PendingDevice 模型字段名
|
||||
const fieldMapping = {
|
||||
'name': 'deviceName',
|
||||
'type': 'deviceType',
|
||||
'SN': 'serialNumber',
|
||||
name: 'deviceName',
|
||||
type: 'deviceType',
|
||||
SN: 'serialNumber',
|
||||
};
|
||||
|
||||
|
||||
// 确保使用 deviceFields 中定义的字段名来设置表单值
|
||||
const formValues = {
|
||||
serialNumber: record.serialNumber,
|
||||
roomId: record.roomId,
|
||||
rackId: record.rackId,
|
||||
};
|
||||
|
||||
|
||||
// 使用已加载的字段配置或默认配置
|
||||
const fields = deviceFields || defaultDeviceFields;
|
||||
|
||||
|
||||
// 遍历字段,将 record 中对应的值设置到表单
|
||||
fields.forEach(field => {
|
||||
const fieldName = field.fieldName;
|
||||
@@ -249,7 +355,7 @@ const PendingDeviceManagement = () => {
|
||||
formValues[fieldName] = record[recordFieldName];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 处理自定义字段
|
||||
if (record.customFields) {
|
||||
Object.entries(record.customFields).forEach(([key, value]) => {
|
||||
@@ -258,21 +364,21 @@ const PendingDeviceManagement = () => {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
form.setFieldsValue(formValues);
|
||||
setEditModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEditSubmit = async (values) => {
|
||||
const handleEditSubmit = async values => {
|
||||
try {
|
||||
const { purchaseDate, warrantyExpiry, ...otherValues } = values;
|
||||
|
||||
|
||||
const payload = {
|
||||
...otherValues,
|
||||
purchaseDate: purchaseDate ? dayjs(purchaseDate).toISOString() : null,
|
||||
warrantyExpiry: warrantyExpiry ? dayjs(warrantyExpiry).toISOString() : null
|
||||
warrantyExpiry: warrantyExpiry ? dayjs(warrantyExpiry).toISOString() : null,
|
||||
};
|
||||
|
||||
|
||||
await api.put(`/inventory/pending-devices/${currentDevice.pendingId}`, payload);
|
||||
message.success('更新成功');
|
||||
setEditModalVisible(false);
|
||||
@@ -283,7 +389,7 @@ const PendingDeviceManagement = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (pendingId) => {
|
||||
const handleDelete = async pendingId => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个待同步设备吗?此操作不可恢复!',
|
||||
@@ -303,16 +409,20 @@ const PendingDeviceManagement = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusTag = (status) => {
|
||||
const getStatusTag = status => {
|
||||
const statusMap = {
|
||||
pending: { color: 'processing', text: '待同步', icon: <SyncOutlined spin /> },
|
||||
synced: { color: 'success', text: '已同步', icon: <CheckCircleOutlined /> },
|
||||
};
|
||||
const config = statusMap[status] || statusMap.pending;
|
||||
return <Tag color={config.color} icon={config.icon}>{config.text}</Tag>;
|
||||
return (
|
||||
<Tag color={config.color} icon={config.icon}>
|
||||
{config.text}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
const getTypeLabel = (type) => {
|
||||
const getTypeLabel = type => {
|
||||
const typeMap = {
|
||||
server: '服务器',
|
||||
switch: '交换机',
|
||||
@@ -323,7 +433,7 @@ const PendingDeviceManagement = () => {
|
||||
return typeMap[type] || type;
|
||||
};
|
||||
|
||||
const renderFormField = (field) => {
|
||||
const renderFormField = field => {
|
||||
const { fieldName, displayName, fieldType, required, options } = field;
|
||||
|
||||
if (fieldName === 'roomId') {
|
||||
@@ -334,12 +444,12 @@ const PendingDeviceManagement = () => {
|
||||
label={displayName}
|
||||
rules={required ? [{ required: true, message: `请选择${displayName}` }] : []}
|
||||
>
|
||||
<Select
|
||||
placeholder={`请选择${displayName}`}
|
||||
allowClear
|
||||
showSearch
|
||||
<Select
|
||||
placeholder={`请选择${displayName}`}
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
onChange={(value) => {
|
||||
onChange={value => {
|
||||
setSelectedRoomId(value);
|
||||
form.setFieldsValue({ rackId: undefined });
|
||||
}}
|
||||
@@ -351,7 +461,9 @@ const PendingDeviceManagement = () => {
|
||||
</Select.Option>
|
||||
))
|
||||
) : (
|
||||
<Select.Option value="" disabled>暂无机房数据</Select.Option>
|
||||
<Select.Option value="" disabled>
|
||||
暂无机房数据
|
||||
</Select.Option>
|
||||
)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
@@ -367,10 +479,10 @@ const PendingDeviceManagement = () => {
|
||||
label={displayName}
|
||||
rules={required ? [{ required: true, message: `请选择${displayName}` }] : []}
|
||||
>
|
||||
<Select
|
||||
placeholder={selectedRoomId ? `请选择${displayName}` : "请先选择机房"}
|
||||
allowClear
|
||||
showSearch
|
||||
<Select
|
||||
placeholder={selectedRoomId ? `请选择${displayName}` : '请先选择机房'}
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
disabled={!selectedRoomId}
|
||||
>
|
||||
@@ -382,10 +494,14 @@ const PendingDeviceManagement = () => {
|
||||
</Select.Option>
|
||||
))
|
||||
) : (
|
||||
<Select.Option value="" disabled>该机房下无机柜</Select.Option>
|
||||
<Select.Option value="" disabled>
|
||||
该机房下无机柜
|
||||
</Select.Option>
|
||||
)
|
||||
) : (
|
||||
<Select.Option value="" disabled>请先选择机房</Select.Option>
|
||||
<Select.Option value="" disabled>
|
||||
请先选择机房
|
||||
</Select.Option>
|
||||
)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
@@ -401,13 +517,16 @@ const PendingDeviceManagement = () => {
|
||||
rules={required ? [{ required: true, message: `请选择${displayName}` }] : []}
|
||||
>
|
||||
<Select placeholder={`请选择${displayName}`}>
|
||||
{(Array.isArray(options) ? options : [
|
||||
{ value: 'server', label: '服务器' },
|
||||
{ value: 'switch', label: '交换机' },
|
||||
{ value: 'router', label: '路由器' },
|
||||
{ value: 'storage', label: '存储设备' },
|
||||
{ value: 'other', label: '其他' },
|
||||
]).map(opt => (
|
||||
{(Array.isArray(options)
|
||||
? options
|
||||
: [
|
||||
{ value: 'server', label: '服务器' },
|
||||
{ value: 'switch', label: '交换机' },
|
||||
{ value: 'router', label: '路由器' },
|
||||
{ value: 'storage', label: '存储设备' },
|
||||
{ value: 'other', label: '其他' },
|
||||
]
|
||||
).map(opt => (
|
||||
<Select.Option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</Select.Option>
|
||||
@@ -490,11 +609,11 @@ const PendingDeviceManagement = () => {
|
||||
const renderFormFields = () => {
|
||||
// 如果字段配置未加载,使用默认配置
|
||||
const fields = deviceFields || defaultDeviceFields;
|
||||
|
||||
|
||||
// 排除机房和机柜字段(已在Modal中单独渲染)
|
||||
const textFields = []; // 描述、备注等全文本字段
|
||||
const otherFields = []; // 其他字段
|
||||
|
||||
|
||||
fields.forEach(field => {
|
||||
// 排除 roomId 和 rackId
|
||||
if (field.fieldName === 'roomId' || field.fieldName === 'rackId') {
|
||||
@@ -514,15 +633,11 @@ const PendingDeviceManagement = () => {
|
||||
{/* 其他字段两列布局 */}
|
||||
{otherFields.length > 0 && (
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
{otherFields.filter((_, i) => i % 2 === 0).map(renderFormField)}
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
{otherFields.filter((_, i) => i % 2 === 1).map(renderFormField)}
|
||||
</Col>
|
||||
<Col span={12}>{otherFields.filter((_, i) => i % 2 === 0).map(renderFormField)}</Col>
|
||||
<Col span={12}>{otherFields.filter((_, i) => i % 2 === 1).map(renderFormField)}</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
|
||||
{/* 文本字段全文本显示 */}
|
||||
{textFields.map(renderFormField)}
|
||||
</>
|
||||
@@ -548,7 +663,7 @@ const PendingDeviceManagement = () => {
|
||||
dataIndex: 'deviceType',
|
||||
key: 'deviceType',
|
||||
width: 100,
|
||||
render: (type) => getTypeLabel(type),
|
||||
render: type => getTypeLabel(type),
|
||||
},
|
||||
{
|
||||
title: '位置',
|
||||
@@ -556,8 +671,8 @@ const PendingDeviceManagement = () => {
|
||||
width: 180,
|
||||
render: (_, record) => (
|
||||
<span>
|
||||
{record.Room?.name || '-'}
|
||||
{record.Rack ? ` / ${record.Rack.name}` : ''}
|
||||
{record.Room?.name || '-'}
|
||||
{record.Rack ? ` / ${record.Rack.name}` : ''}
|
||||
{record.position ? ` / U${record.position}` : ''}
|
||||
</span>
|
||||
),
|
||||
@@ -567,35 +682,35 @@ const PendingDeviceManagement = () => {
|
||||
dataIndex: ['Plan', 'name'],
|
||||
key: 'planName',
|
||||
width: 150,
|
||||
render: (name) => name || '-',
|
||||
render: name => name || '-',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (status) => getStatusTag(status),
|
||||
render: status => getStatusTag(status),
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
dataIndex: ['Creator', 'realName'],
|
||||
key: 'creator',
|
||||
width: 100,
|
||||
render: (name) => name || '-',
|
||||
render: name => name || '-',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 140,
|
||||
render: (date) => date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '-',
|
||||
render: date => (date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '-'),
|
||||
},
|
||||
{
|
||||
title: '同步时间',
|
||||
dataIndex: 'syncedAt',
|
||||
key: 'syncedAt',
|
||||
width: 140,
|
||||
render: (date) => date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '-',
|
||||
render: date => (date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '-'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
@@ -641,11 +756,7 @@ const PendingDeviceManagement = () => {
|
||||
cancelText="取消"
|
||||
>
|
||||
<Tooltip title="删除">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DeleteOutlined />}
|
||||
style={{ color: '#ff4d4f' }}
|
||||
/>
|
||||
<Button type="text" icon={<DeleteOutlined />} style={{ color: '#ff4d4f' }} />
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
@@ -654,15 +765,17 @@ const PendingDeviceManagement = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24, background: designTokens.colors.background.secondary, minHeight: '100vh' }}>
|
||||
<div
|
||||
style={{
|
||||
padding: 24,
|
||||
background: designTokens.colors.background.secondary,
|
||||
minHeight: '100vh',
|
||||
}}
|
||||
>
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col span={6}>
|
||||
<Card bordered={false} style={{ borderRadius: 12, height: '100%', minHeight: 100 }}>
|
||||
<Statistic
|
||||
title="暂存设备总数"
|
||||
value={stats.total}
|
||||
prefix={<InboxOutlined />}
|
||||
/>
|
||||
<Statistic title="暂存设备总数" value={stats.total} prefix={<InboxOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
@@ -692,8 +805,8 @@ const PendingDeviceManagement = () => {
|
||||
value={stats.total > 0 ? Math.round((stats.synced / stats.total) * 100) : 0}
|
||||
suffix="%"
|
||||
/>
|
||||
<Progress
|
||||
percent={stats.total > 0 ? Math.round((stats.synced / stats.total) * 100) : 0}
|
||||
<Progress
|
||||
percent={stats.total > 0 ? Math.round((stats.synced / stats.total) * 100) : 0}
|
||||
showInfo={false}
|
||||
strokeColor="#52c41a"
|
||||
/>
|
||||
@@ -702,14 +815,23 @@ const PendingDeviceManagement = () => {
|
||||
</Row>
|
||||
|
||||
<Card bordered={false} style={{ borderRadius: 12 }}>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Input
|
||||
placeholder="搜索序列号/设备名称"
|
||||
prefix={<SearchOutlined />}
|
||||
style={{ width: 200 }}
|
||||
value={filters.keyword}
|
||||
onChange={(e) => setFilters(prev => ({ ...prev, keyword: e.target.value }))}
|
||||
onChange={e => setFilters(prev => ({ ...prev, keyword: e.target.value }))}
|
||||
onPressEnter={() => setPagination(prev => ({ ...prev, current: 1 }))}
|
||||
/>
|
||||
<Select
|
||||
@@ -717,7 +839,7 @@ const PendingDeviceManagement = () => {
|
||||
style={{ width: 120 }}
|
||||
allowClear
|
||||
value={filters.status}
|
||||
onChange={(value) => setFilters(prev => ({ ...prev, status: value }))}
|
||||
onChange={value => setFilters(prev => ({ ...prev, status: value }))}
|
||||
>
|
||||
<Select.Option value="pending">待同步</Select.Option>
|
||||
<Select.Option value="synced">已同步</Select.Option>
|
||||
@@ -729,7 +851,7 @@ const PendingDeviceManagement = () => {
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
value={filters.planId}
|
||||
onChange={(value) => setFilters(prev => ({ ...prev, planId: value }))}
|
||||
onChange={value => setFilters(prev => ({ ...prev, planId: value }))}
|
||||
>
|
||||
{plans.map(plan => (
|
||||
<Select.Option key={plan.planId} value={plan.planId}>
|
||||
@@ -762,7 +884,7 @@ const PendingDeviceManagement = () => {
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: setSelectedRowKeys,
|
||||
getCheckboxProps: (record) => ({
|
||||
getCheckboxProps: record => ({
|
||||
disabled: record.status !== 'pending',
|
||||
}),
|
||||
}}
|
||||
@@ -774,7 +896,7 @@ const PendingDeviceManagement = () => {
|
||||
...pagination,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
showTotal: total => `共 ${total} 条`,
|
||||
onChange: (page, pageSize) => {
|
||||
setPagination(prev => ({ ...prev, current: page, pageSize }));
|
||||
},
|
||||
@@ -797,11 +919,7 @@ const PendingDeviceManagement = () => {
|
||||
footer={null}
|
||||
width={600}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleEditSubmit}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleEditSubmit}>
|
||||
<Descriptions bordered size="small" column={2} style={{ marginBottom: 16 }}>
|
||||
<Descriptions.Item label="序列号">
|
||||
<code>{currentDevice?.serialNumber}</code>
|
||||
@@ -814,16 +932,13 @@ const PendingDeviceManagement = () => {
|
||||
{/* 机房机柜选择 - 始终显示 */}
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="roomId"
|
||||
label="所属机房"
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择机房"
|
||||
allowClear
|
||||
showSearch
|
||||
<Form.Item name="roomId" label="所属机房">
|
||||
<Select
|
||||
placeholder="请选择机房"
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
onChange={(value) => {
|
||||
onChange={value => {
|
||||
setSelectedRoomId(value);
|
||||
form.setFieldsValue({ rackId: undefined });
|
||||
}}
|
||||
@@ -835,20 +950,19 @@ const PendingDeviceManagement = () => {
|
||||
</Select.Option>
|
||||
))
|
||||
) : (
|
||||
<Select.Option value="" disabled>暂无机房数据</Select.Option>
|
||||
<Select.Option value="" disabled>
|
||||
暂无机房数据
|
||||
</Select.Option>
|
||||
)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="rackId"
|
||||
label="所属机柜"
|
||||
>
|
||||
<Select
|
||||
placeholder={selectedRoomId ? "请选择机柜" : "请先选择机房"}
|
||||
allowClear
|
||||
showSearch
|
||||
<Form.Item name="rackId" label="所属机柜">
|
||||
<Select
|
||||
placeholder={selectedRoomId ? '请选择机柜' : '请先选择机房'}
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
disabled={!selectedRoomId}
|
||||
>
|
||||
@@ -860,10 +974,14 @@ const PendingDeviceManagement = () => {
|
||||
</Select.Option>
|
||||
))
|
||||
) : (
|
||||
<Select.Option value="" disabled>该机房下无机柜</Select.Option>
|
||||
<Select.Option value="" disabled>
|
||||
该机房下无机柜
|
||||
</Select.Option>
|
||||
)
|
||||
) : (
|
||||
<Select.Option value="" disabled>请先选择机房</Select.Option>
|
||||
<Select.Option value="" disabled>
|
||||
请先选择机房
|
||||
</Select.Option>
|
||||
)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
@@ -874,10 +992,14 @@ const PendingDeviceManagement = () => {
|
||||
|
||||
<Form.Item style={{ marginBottom: 0, textAlign: 'right', marginTop: 16 }}>
|
||||
<Space>
|
||||
<Button onClick={() => {
|
||||
setEditModalVisible(false);
|
||||
setSelectedRoomId(null);
|
||||
}}>取消</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setEditModalVisible(false);
|
||||
setSelectedRoomId(null);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
保存
|
||||
</Button>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -371,7 +371,7 @@ const Rack3DVisualization = () => {
|
||||
|
||||
// 计算已用U位(考虑设备高度)
|
||||
const usedU = devices.reduce((sum, device) => sum + (device.height || 1), 0);
|
||||
|
||||
|
||||
// 计算总功率
|
||||
const totalPower = devices.reduce((sum, device) => {
|
||||
const power = parseFloat(device.powerConsumption) || 0;
|
||||
@@ -380,7 +380,7 @@ const Rack3DVisualization = () => {
|
||||
|
||||
// 机柜总高度
|
||||
const totalHeight = selectedRack.height || 45;
|
||||
|
||||
|
||||
// 可用U位
|
||||
const availableU = totalHeight - usedU;
|
||||
|
||||
@@ -568,7 +568,8 @@ const Rack3DVisualization = () => {
|
||||
position: 'absolute',
|
||||
top: 88,
|
||||
right: 24,
|
||||
background: 'linear-gradient(135deg, rgba(15, 23, 42, 0.95) 0%, rgba(30, 41, 59, 0.9) 100%)',
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(15, 23, 42, 0.95) 0%, rgba(30, 41, 59, 0.9) 100%)',
|
||||
backdropFilter: 'blur(16px)',
|
||||
padding: isRackInfoCollapsed ? '14px 18px' : '20px',
|
||||
borderRadius: '20px',
|
||||
@@ -615,10 +616,13 @@ const Rack3DVisualization = () => {
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '50%',
|
||||
background: 'linear-gradient(180deg, rgba(255,255,255,0.15) 0%, transparent 100%)',
|
||||
background:
|
||||
'linear-gradient(180deg, rgba(255,255,255,0.15) 0%, transparent 100%)',
|
||||
}}
|
||||
/>
|
||||
<CloudServerOutlined style={{ color: 'white', fontSize: 22, position: 'relative', zIndex: 1 }} />
|
||||
<CloudServerOutlined
|
||||
style={{ color: 'white', fontSize: 22, position: 'relative', zIndex: 1 }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
@@ -640,13 +644,15 @@ const Rack3DVisualization = () => {
|
||||
>
|
||||
{selectedRack.name}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: 'rgba(148, 163, 184, 0.8)',
|
||||
marginTop: 4,
|
||||
fontFamily: 'monospace',
|
||||
letterSpacing: '0.5px',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: 'rgba(148, 163, 184, 0.8)',
|
||||
marginTop: 4,
|
||||
fontFamily: 'monospace',
|
||||
letterSpacing: '0.5px',
|
||||
}}
|
||||
>
|
||||
#{selectedRack.rackId}
|
||||
</div>
|
||||
</div>
|
||||
@@ -660,7 +666,7 @@ const Rack3DVisualization = () => {
|
||||
e.stopPropagation();
|
||||
setIsRackInfoCollapsed(!isRackInfoCollapsed);
|
||||
}}
|
||||
style={{
|
||||
style={{
|
||||
color: 'rgba(148, 163, 184, 0.7)',
|
||||
marginLeft: isRackInfoCollapsed ? 0 : 8,
|
||||
width: isRackInfoCollapsed ? 44 : 32,
|
||||
@@ -701,10 +707,24 @@ const Rack3DVisualization = () => {
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '22px', fontWeight: 700, color: '#60a5fa', lineHeight: 1 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '22px',
|
||||
fontWeight: 700,
|
||||
color: '#60a5fa',
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{selectedRack.height}
|
||||
</div>
|
||||
<div style={{ fontSize: '10px', color: 'rgba(148, 163, 184, 0.7)', marginTop: 4, fontWeight: 500 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
color: 'rgba(148, 163, 184, 0.7)',
|
||||
marginTop: 4,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
总高度
|
||||
</div>
|
||||
</div>
|
||||
@@ -717,10 +737,24 @@ const Rack3DVisualization = () => {
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '22px', fontWeight: 700, color: '#34d399', lineHeight: 1 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '22px',
|
||||
fontWeight: 700,
|
||||
color: '#34d399',
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{devices.length}
|
||||
</div>
|
||||
<div style={{ fontSize: '10px', color: 'rgba(148, 163, 184, 0.7)', marginTop: 4, fontWeight: 500 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
color: 'rgba(148, 163, 184, 0.7)',
|
||||
marginTop: 4,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
设备数
|
||||
</div>
|
||||
</div>
|
||||
@@ -733,10 +767,24 @@ const Rack3DVisualization = () => {
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '22px', fontWeight: 700, color: '#fbbf24', lineHeight: 1 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '22px',
|
||||
fontWeight: 700,
|
||||
color: '#fbbf24',
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{Math.round((devices.length / selectedRack.height) * 100)}%
|
||||
</div>
|
||||
<div style={{ fontSize: '10px', color: 'rgba(148, 163, 184, 0.7)', marginTop: 4, fontWeight: 500 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
color: 'rgba(148, 163, 184, 0.7)',
|
||||
marginTop: 4,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
负载率
|
||||
</div>
|
||||
</div>
|
||||
@@ -752,49 +800,86 @@ const Rack3DVisualization = () => {
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '11px', color: 'rgba(148, 163, 184, 0.6)', marginBottom: 12, fontWeight: 600, letterSpacing: '0.5px', textTransform: 'uppercase' }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: 'rgba(148, 163, 184, 0.6)',
|
||||
marginBottom: 12,
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.5px',
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
资源监控
|
||||
</div>
|
||||
{[
|
||||
{
|
||||
label: 'U位使用',
|
||||
value: `${rackStats.usedU}U / ${selectedRack.height}U`,
|
||||
status: rackStats.usagePercent > 80 ? 'warning' : 'normal',
|
||||
{
|
||||
label: 'U位使用',
|
||||
value: `${rackStats.usedU}U / ${selectedRack.height}U`,
|
||||
status: rackStats.usagePercent > 80 ? 'warning' : 'normal',
|
||||
icon: '📊',
|
||||
subValue: `剩余 ${rackStats.availableU}U`,
|
||||
},
|
||||
{
|
||||
label: '总功率',
|
||||
value: `${rackStats.totalPower.toFixed(1)}kW`,
|
||||
status: 'normal',
|
||||
{
|
||||
label: '总功率',
|
||||
value: `${rackStats.totalPower.toFixed(1)}kW`,
|
||||
status: 'normal',
|
||||
icon: '⚡',
|
||||
subValue: '当前负载',
|
||||
},
|
||||
{
|
||||
label: '设备数',
|
||||
value: `${devices.length} 台`,
|
||||
status: 'normal',
|
||||
{
|
||||
label: '设备数',
|
||||
value: `${devices.length} 台`,
|
||||
status: 'normal',
|
||||
icon: '🖥️',
|
||||
subValue: `负载率 ${rackStats.usagePercent}%`,
|
||||
},
|
||||
].map((item, i) => (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: i < 2 ? 12 : 0 }}>
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: i < 2 ? 12 : 0,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<span style={{ fontSize: 16 }}>{item.icon}</span>
|
||||
<div>
|
||||
<div style={{ fontSize: '13px', color: 'rgba(226, 232, 240, 0.9)', fontWeight: 500 }}>{item.label}</div>
|
||||
<div style={{ fontSize: '11px', color: 'rgba(148, 163, 184, 0.6)', marginTop: 2 }}>{item.subValue}</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
color: 'rgba(226, 232, 240, 0.9)',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: 'rgba(148, 163, 184, 0.6)',
|
||||
marginTop: 2,
|
||||
}}
|
||||
>
|
||||
{item.subValue}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<span style={{ fontSize: '14px', fontWeight: 700, color: '#f8fafc' }}>{item.value}</span>
|
||||
<div style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
background: item.status === 'normal' ? '#10b981' : '#f59e0b',
|
||||
boxShadow: `0 0 8px ${item.status === 'normal' ? 'rgba(16, 185, 129, 0.6)' : 'rgba(245, 158, 11, 0.6)'}`,
|
||||
}} />
|
||||
<span style={{ fontSize: '14px', fontWeight: 700, color: '#f8fafc' }}>
|
||||
{item.value}
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
background: item.status === 'normal' ? '#10b981' : '#f59e0b',
|
||||
boxShadow: `0 0 8px ${item.status === 'normal' ? 'rgba(16, 185, 129, 0.6)' : 'rgba(245, 158, 11, 0.6)'}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -845,13 +930,15 @@ const Rack3DVisualization = () => {
|
||||
>
|
||||
<span style={{ fontSize: 14 }}>{item.icon}</span>
|
||||
<span style={{ flex: 1 }}>{item.text}</span>
|
||||
<div style={{
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: '50%',
|
||||
background: item.color,
|
||||
boxShadow: `0 0 6px ${item.color}`,
|
||||
}} />
|
||||
<div
|
||||
style={{
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: '50%',
|
||||
background: item.color,
|
||||
boxShadow: `0 0 6px ${item.color}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -439,7 +439,9 @@ function RackManagement() {
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
const results = await Promise.allSettled(selectedRackIds.map(id => axios.delete(`/api/racks/${id}`)));
|
||||
const results = await Promise.allSettled(
|
||||
selectedRackIds.map(id => axios.delete(`/api/racks/${id}`))
|
||||
);
|
||||
const succeeded = results.filter(r => r.status === 'fulfilled').length;
|
||||
const failed = results.filter(r => r.status === 'rejected');
|
||||
if (succeeded > 0) {
|
||||
@@ -537,14 +539,14 @@ function RackManagement() {
|
||||
failedCount: 0,
|
||||
errors: [],
|
||||
createdRacks: resData.createdRacks || [],
|
||||
skippedRacks: resData.skippedRacks || []
|
||||
skippedRacks: resData.skippedRacks || [],
|
||||
};
|
||||
|
||||
if (resData.details && Array.isArray(resData.details)) {
|
||||
importResult.failedCount = resData.details.length;
|
||||
importResult.errors = resData.details.map(item => ({
|
||||
row: item.row,
|
||||
error: item.errors.join(';')
|
||||
error: item.errors.join(';'),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -571,8 +573,8 @@ function RackManagement() {
|
||||
failedCount: errorData.details.length,
|
||||
errors: errorData.details.map(item => ({
|
||||
row: item.row,
|
||||
error: item.errors.join(';')
|
||||
}))
|
||||
error: item.errors.join(';'),
|
||||
})),
|
||||
};
|
||||
setImportResult(importResult);
|
||||
setImportPhase('导入失败');
|
||||
@@ -654,7 +656,9 @@ function RackManagement() {
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
sorter: (a, b) => (a.Devices?.reduce((sum, d) => sum + (d.height || 1), 0) || 0) - (b.Devices?.reduce((sum, d) => sum + (d.height || 1), 0) || 0),
|
||||
sorter: (a, b) =>
|
||||
(a.Devices?.reduce((sum, d) => sum + (d.height || 1), 0) || 0) -
|
||||
(b.Devices?.reduce((sum, d) => sum + (d.height || 1), 0) || 0),
|
||||
},
|
||||
{
|
||||
title: '功率使用',
|
||||
@@ -1437,14 +1441,16 @@ function RackManagement() {
|
||||
<p style={{ fontWeight: '600', marginBottom: '8px', color: '#52c41a' }}>
|
||||
✓ 本次新增机柜({importResult.createdRacks.length}):
|
||||
</p>
|
||||
<div style={{
|
||||
maxHeight: '150px',
|
||||
overflow: 'auto',
|
||||
background: '#f6ffed',
|
||||
border: '1px solid #b7eb8f',
|
||||
borderRadius: '8px',
|
||||
padding: '12px'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
maxHeight: '150px',
|
||||
overflow: 'auto',
|
||||
background: '#f6ffed',
|
||||
border: '1px solid #b7eb8f',
|
||||
borderRadius: '8px',
|
||||
padding: '12px',
|
||||
}}
|
||||
>
|
||||
{importResult.createdRacks.map((rack, idx) => (
|
||||
<div key={idx} style={{ fontSize: '13px', marginBottom: '4px' }}>
|
||||
• {rack.rackId} - {rack.name}
|
||||
@@ -1459,14 +1465,16 @@ function RackManagement() {
|
||||
<p style={{ fontWeight: '600', marginBottom: '8px', color: '#faad14' }}>
|
||||
⚠ 已跳过机柜({importResult.skippedRacks.length}):
|
||||
</p>
|
||||
<div style={{
|
||||
maxHeight: '150px',
|
||||
overflow: 'auto',
|
||||
background: '#fffbe6',
|
||||
border: '1px solid #ffe58f',
|
||||
borderRadius: '8px',
|
||||
padding: '12px'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
maxHeight: '150px',
|
||||
overflow: 'auto',
|
||||
background: '#fffbe6',
|
||||
border: '1px solid #ffe58f',
|
||||
borderRadius: '8px',
|
||||
padding: '12px',
|
||||
}}
|
||||
>
|
||||
{importResult.skippedRacks.map((rack, idx) => (
|
||||
<div key={idx} style={{ fontSize: '13px', marginBottom: '4px' }}>
|
||||
• {rack.rackId} - {rack.name}
|
||||
@@ -1478,7 +1486,9 @@ function RackManagement() {
|
||||
|
||||
{importResult.errors && importResult.errors.length > 0 && (
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<p style={{ fontWeight: '600', marginBottom: '8px', color: '#ff4d4f' }}>错误详情:</p>
|
||||
<p style={{ fontWeight: '600', marginBottom: '8px', color: '#ff4d4f' }}>
|
||||
错误详情:
|
||||
</p>
|
||||
{importResult.errors.slice(0, 5).map((err, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,13 @@ import {
|
||||
InputNumber,
|
||||
Switch,
|
||||
} from 'antd';
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, PlusCircleOutlined, MinusCircleOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
PlusCircleOutlined,
|
||||
MinusCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import CloseButton from '../components/CloseButton';
|
||||
|
||||
@@ -37,44 +43,46 @@ const OptionsEditor = ({ value, onChange }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
background: 'linear-gradient(135deg, #fafbfc 0%, #f5f7fa 100%)',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: '16px',
|
||||
gap: '8px',
|
||||
}}>
|
||||
<div style={{
|
||||
width: '4px',
|
||||
height: '16px',
|
||||
background: 'linear-gradient(180deg, #667eea 0%, #764ba2 100%)',
|
||||
borderRadius: '2px',
|
||||
}}/>
|
||||
<span style={{ color: '#333', fontSize: '14px', fontWeight: '600' }}>
|
||||
选项配置
|
||||
</span>
|
||||
<span style={{ color: '#999', fontSize: '12px' }}>
|
||||
(值用于提交,标签用于显示)
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
background: 'linear-gradient(135deg, #fafbfc 0%, #f5f7fa 100%)',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: '16px',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '4px',
|
||||
height: '16px',
|
||||
background: 'linear-gradient(180deg, #667eea 0%, #764ba2 100%)',
|
||||
borderRadius: '2px',
|
||||
}}
|
||||
/>
|
||||
<span style={{ color: '#333', fontSize: '14px', fontWeight: '600' }}>选项配置</span>
|
||||
<span style={{ color: '#999', fontSize: '12px' }}>(值用于提交,标签用于显示)</span>
|
||||
</div>
|
||||
|
||||
{options.length === 0 ? (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '24px',
|
||||
background: '#fff',
|
||||
borderRadius: '8px',
|
||||
border: '1px dashed #d9d9d9',
|
||||
}}>
|
||||
<div style={{ color: '#bbb', fontSize: '14px', marginBottom: '12px' }}>
|
||||
暂无选项
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: '24px',
|
||||
background: '#fff',
|
||||
borderRadius: '8px',
|
||||
border: '1px dashed #d9d9d9',
|
||||
}}
|
||||
>
|
||||
<div style={{ color: '#bbb', fontSize: '14px', marginBottom: '12px' }}>暂无选项</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusCircleOutlined />}
|
||||
@@ -90,15 +98,23 @@ const OptionsEditor = ({ value, onChange }) => {
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '16px' }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
padding: '0 4px',
|
||||
marginBottom: '4px',
|
||||
}}>
|
||||
<span style={{ width: '160px', color: '#666', fontSize: '12px', fontWeight: '500' }}>值(value)</span>
|
||||
<span style={{ width: '160px', color: '#666', fontSize: '12px', fontWeight: '500' }}>标签(label)</span>
|
||||
<div
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '16px' }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
padding: '0 4px',
|
||||
marginBottom: '4px',
|
||||
}}
|
||||
>
|
||||
<span style={{ width: '160px', color: '#666', fontSize: '12px', fontWeight: '500' }}>
|
||||
值(value)
|
||||
</span>
|
||||
<span style={{ width: '160px', color: '#666', fontSize: '12px', fontWeight: '500' }}>
|
||||
标签(label)
|
||||
</span>
|
||||
</div>
|
||||
{value.map((opt, index) => (
|
||||
<div
|
||||
@@ -114,19 +130,21 @@ const OptionsEditor = ({ value, onChange }) => {
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #667eea20 0%, #764ba220 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#667eea',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #667eea20 0%, #764ba220 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#667eea',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
<Input
|
||||
@@ -432,11 +450,7 @@ function TicketFieldManagement() {
|
||||
<OptionsEditor />
|
||||
</Form.Item>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="options"
|
||||
label="选项配置"
|
||||
tooltip="仅下拉选择类型需要配置选项"
|
||||
>
|
||||
<Form.Item name="options" label="选项配置" tooltip="仅下拉选择类型需要配置选项">
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
placeholder="仅下拉选择类型需要配置,此处不可编辑"
|
||||
|
||||
@@ -360,11 +360,13 @@ function TicketManagement() {
|
||||
const response = await axios.get('/api/ticket-fields');
|
||||
const dbFields = response.data.sort((a, b) => a.order - b.order);
|
||||
|
||||
const isValidOptions = (opts) => {
|
||||
const isValidOptions = opts => {
|
||||
if (!opts) return false;
|
||||
if (!Array.isArray(opts)) return false;
|
||||
if (opts.length === 0) return false;
|
||||
return opts.some(opt => opt && opt.value !== undefined && opt.value !== null && opt.value !== '');
|
||||
return opts.some(
|
||||
opt => opt && opt.value !== undefined && opt.value !== null && opt.value !== ''
|
||||
);
|
||||
};
|
||||
|
||||
const mergedFields = DEFAULT_TICKET_FIELDS.map(defaultField => {
|
||||
@@ -468,7 +470,11 @@ function TicketManagement() {
|
||||
break;
|
||||
case 'textarea':
|
||||
formItem = (
|
||||
<Input.TextArea rows={3} placeholder={placeholder || `请输入${displayName}`} showCount />
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
placeholder={placeholder || `请输入${displayName}`}
|
||||
showCount
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'boolean':
|
||||
@@ -1131,17 +1137,19 @@ function TicketManagement() {
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<div style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 8,
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 8,
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />
|
||||
</div>
|
||||
<span style={{ fontWeight: 600, fontSize: 18 }}>
|
||||
@@ -1166,32 +1174,38 @@ function TicketManagement() {
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
{!editingTicket && (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #f0f4ff 0%, #fafbff 100%)',
|
||||
border: '1px solid #e8eaff',
|
||||
borderRadius: 12,
|
||||
padding: '12px 16px',
|
||||
marginBottom: 20,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
}}>
|
||||
<div style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 8,
|
||||
background: '#667eea',
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #f0f4ff 0%, #fafbff 100%)',
|
||||
border: '1px solid #e8eaff',
|
||||
borderRadius: 12,
|
||||
padding: '12px 16px',
|
||||
marginBottom: 20,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
}}>
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 8,
|
||||
background: '#667eea',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
TKT
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#666', marginBottom: 2 }}>工单编号(自动生成)</div>
|
||||
<div style={{ fontSize: 12, color: '#666', marginBottom: 2 }}>
|
||||
工单编号(自动生成)
|
||||
</div>
|
||||
<Form.Item name="ticketId" noStyle>
|
||||
<Input
|
||||
disabled
|
||||
@@ -1209,28 +1223,34 @@ function TicketManagement() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '0 24px',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '0 24px',
|
||||
}}
|
||||
>
|
||||
<div style={{ gridColumn: '1 / -1', marginBottom: 8 }}>
|
||||
<div style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: '#333',
|
||||
marginBottom: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}>
|
||||
<span style={{
|
||||
width: 4,
|
||||
height: 16,
|
||||
background: 'linear-gradient(180deg, #667eea 0%, #764ba2 100%)',
|
||||
borderRadius: 2,
|
||||
display: 'inline-block',
|
||||
}} />
|
||||
<div
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: '#333',
|
||||
marginBottom: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 4,
|
||||
height: 16,
|
||||
background: 'linear-gradient(180deg, #667eea 0%, #764ba2 100%)',
|
||||
borderRadius: 2,
|
||||
display: 'inline-block',
|
||||
}}
|
||||
/>
|
||||
设备信息
|
||||
</div>
|
||||
</div>
|
||||
@@ -1264,7 +1284,11 @@ function TicketManagement() {
|
||||
<div>
|
||||
<Form.Item
|
||||
name="serialNumber"
|
||||
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: '请输入设备序列号' }]}
|
||||
>
|
||||
<Input placeholder="请输入设备序列号" size="large" />
|
||||
@@ -1283,7 +1307,11 @@ function TicketManagement() {
|
||||
<div style={{ gridColumn: '1 / -1' }}>
|
||||
<Form.Item
|
||||
name="deviceId"
|
||||
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: '请选择关联设备' }]}
|
||||
>
|
||||
<Select
|
||||
@@ -1317,11 +1345,7 @@ function TicketManagement() {
|
||||
return renderDeviceSection();
|
||||
} else if (hasDeviceNameField) {
|
||||
if (!ticketFields.some(f => f.fieldName === 'deviceId')) {
|
||||
return (
|
||||
<div style={{ gridColumn: '1 / -1' }}>
|
||||
{renderDeviceSection()}
|
||||
</div>
|
||||
);
|
||||
return <div style={{ gridColumn: '1 / -1' }}>{renderDeviceSection()}</div>;
|
||||
}
|
||||
} else {
|
||||
return renderDeviceSection();
|
||||
@@ -1330,38 +1354,56 @@ function TicketManagement() {
|
||||
})()}
|
||||
|
||||
<div style={{ gridColumn: '1 / -1', marginBottom: 8, marginTop: 8 }}>
|
||||
<div style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: '#333',
|
||||
marginBottom: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}>
|
||||
<span style={{
|
||||
width: 4,
|
||||
height: 16,
|
||||
background: 'linear-gradient(180deg, #667eea 0%, #764ba2 100%)',
|
||||
borderRadius: 2,
|
||||
display: 'inline-block',
|
||||
}} />
|
||||
<div
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: '#333',
|
||||
marginBottom: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 4,
|
||||
height: 16,
|
||||
background: 'linear-gradient(180deg, #667eea 0%, #764ba2 100%)',
|
||||
borderRadius: 2,
|
||||
display: 'inline-block',
|
||||
}}
|
||||
/>
|
||||
工单信息
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ticketFields.filter(f =>
|
||||
!['ticketId', 'deviceId', 'deviceName', 'serialNumber', 'expectedCompletionDate', 'resolution', 'notes', 'description'].includes(f.fieldName)
|
||||
).map(field => (
|
||||
<div key={field.fieldName}>
|
||||
{renderFormItem(field)}
|
||||
</div>
|
||||
))}
|
||||
{ticketFields
|
||||
.filter(
|
||||
f =>
|
||||
![
|
||||
'ticketId',
|
||||
'deviceId',
|
||||
'deviceName',
|
||||
'serialNumber',
|
||||
'expectedCompletionDate',
|
||||
'resolution',
|
||||
'notes',
|
||||
'description',
|
||||
].includes(f.fieldName)
|
||||
)
|
||||
.map(field => (
|
||||
<div key={field.fieldName}>{renderFormItem(field)}</div>
|
||||
))}
|
||||
|
||||
<div style={{ gridColumn: '1 / -1' }}>
|
||||
<Form.Item
|
||||
name="title"
|
||||
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: '请输入工单标题' }]}
|
||||
>
|
||||
<Input placeholder="请输入工单标题" size="large" />
|
||||
@@ -1371,7 +1413,11 @@ function TicketManagement() {
|
||||
<div>
|
||||
<Form.Item
|
||||
name="faultCategory"
|
||||
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: '请选择故障分类' }]}
|
||||
>
|
||||
<Select placeholder="请选择故障分类" size="large">
|
||||
@@ -1387,22 +1433,34 @@ function TicketManagement() {
|
||||
<div>
|
||||
<Form.Item
|
||||
name="priority"
|
||||
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: '请选择优先级' }]}
|
||||
initialValue="medium"
|
||||
>
|
||||
<Select placeholder="请选择优先级" size="large">
|
||||
<Option value="low">
|
||||
<Tag color="green" style={{ margin: 0 }}>低</Tag>
|
||||
<Tag color="green" style={{ margin: 0 }}>
|
||||
低
|
||||
</Tag>
|
||||
</Option>
|
||||
<Option value="medium">
|
||||
<Tag color="orange" style={{ margin: 0 }}>中</Tag>
|
||||
<Tag color="orange" style={{ margin: 0 }}>
|
||||
中
|
||||
</Tag>
|
||||
</Option>
|
||||
<Option value="high">
|
||||
<Tag color="red" style={{ margin: 0 }}>高</Tag>
|
||||
<Tag color="red" style={{ margin: 0 }}>
|
||||
高
|
||||
</Tag>
|
||||
</Option>
|
||||
<Option value="urgent">
|
||||
<Tag color="magenta" style={{ margin: 0 }}>紧急</Tag>
|
||||
<Tag color="magenta" style={{ margin: 0 }}>
|
||||
紧急
|
||||
</Tag>
|
||||
</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
@@ -1426,7 +1484,11 @@ function TicketManagement() {
|
||||
<div style={{ gridColumn: '1 / -1' }}>
|
||||
<Form.Item
|
||||
name="description"
|
||||
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: '请输入故障描述' }]}
|
||||
>
|
||||
<TextArea
|
||||
@@ -1439,10 +1501,7 @@ function TicketManagement() {
|
||||
</div>
|
||||
|
||||
<div style={{ gridColumn: '1 / -1', marginTop: 8 }}>
|
||||
<Form.Item
|
||||
name="notes"
|
||||
label={<span style={{ fontWeight: 500 }}>备注信息</span>}
|
||||
>
|
||||
<Form.Item name="notes" label={<span style={{ fontWeight: 500 }}>备注信息</span>}>
|
||||
<TextArea
|
||||
rows={2}
|
||||
placeholder="补充说明或其他相关信息(选填)"
|
||||
@@ -1453,14 +1512,16 @@ function TicketManagement() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
marginTop: 24,
|
||||
paddingTop: 20,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 12,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
marginTop: 24,
|
||||
paddingTop: 20,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<Button onClick={handleCancel} size="large" style={{ minWidth: 100 }}>
|
||||
取消
|
||||
</Button>
|
||||
@@ -1706,15 +1767,19 @@ function TicketManagement() {
|
||||
<Row gutter={[24, 16]}>
|
||||
<Col span={12}>
|
||||
<div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>购买日期</div>
|
||||
<div>{selectedTicket.Device.purchaseDate
|
||||
? dayjs(selectedTicket.Device.purchaseDate).format('YYYY-MM-DD')
|
||||
: '-'}</div>
|
||||
<div>
|
||||
{selectedTicket.Device.purchaseDate
|
||||
? dayjs(selectedTicket.Device.purchaseDate).format('YYYY-MM-DD')
|
||||
: '-'}
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>保修到期</div>
|
||||
<div>{selectedTicket.Device.warrantyExpiry
|
||||
? dayjs(selectedTicket.Device.warrantyExpiry).format('YYYY-MM-DD')
|
||||
: '-'}</div>
|
||||
<div>
|
||||
{selectedTicket.Device.warrantyExpiry
|
||||
? dayjs(selectedTicket.Device.warrantyExpiry).format('YYYY-MM-DD')
|
||||
: '-'}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
@@ -1722,28 +1787,35 @@ function TicketManagement() {
|
||||
{/* 描述信息 */}
|
||||
{selectedTicket.Device.description && (
|
||||
<Card title="描述" style={{ marginBottom: 16 }} size="small">
|
||||
<div style={{ whiteSpace: 'pre-wrap' }}>{selectedTicket.Device.description}</div>
|
||||
<div style={{ whiteSpace: 'pre-wrap' }}>
|
||||
{selectedTicket.Device.description}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 自定义字段卡片 */}
|
||||
{selectedTicket.Device.customFields && Object.keys(selectedTicket.Device.customFields).length > 0 && (
|
||||
<Card title="自定义字段" size="small">
|
||||
<Row gutter={[24, 16]}>
|
||||
{Object.entries(selectedTicket.Device.customFields).map(([key, value]) => {
|
||||
// 从 deviceFields 中查找对应的中文显示名称
|
||||
const fieldConfig = deviceFields.find(f => f.fieldName === key);
|
||||
const displayName = fieldConfig?.displayName || key;
|
||||
return (
|
||||
<Col span={8} key={key}>
|
||||
<div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>{displayName}</div>
|
||||
<div style={{ fontWeight: 500 }}>{String(value)}</div>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
</Card>
|
||||
)}
|
||||
{selectedTicket.Device.customFields &&
|
||||
Object.keys(selectedTicket.Device.customFields).length > 0 && (
|
||||
<Card title="自定义字段" size="small">
|
||||
<Row gutter={[24, 16]}>
|
||||
{Object.entries(selectedTicket.Device.customFields).map(
|
||||
([key, value]) => {
|
||||
// 从 deviceFields 中查找对应的中文显示名称
|
||||
const fieldConfig = deviceFields.find(f => f.fieldName === key);
|
||||
const displayName = fieldConfig?.displayName || key;
|
||||
return (
|
||||
<Col span={8} key={key}>
|
||||
<div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>
|
||||
{displayName}
|
||||
</div>
|
||||
<div style={{ fontWeight: 500 }}>{String(value)}</div>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</Row>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</TabPane>
|
||||
)}
|
||||
@@ -1851,9 +1923,7 @@ function TicketManagement() {
|
||||
</div>
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
<span style={{ color: '#666' }}>操作人:</span>
|
||||
<span style={{ fontWeight: 500 }}>
|
||||
{record.operatorName || '-'}
|
||||
</span>
|
||||
<span style={{ fontWeight: 500 }}>{record.operatorName || '-'}</span>
|
||||
</div>
|
||||
{record.operationDescription && (
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { Card, Row, Col, Statistic, Table, DatePicker, Select, Space, Tag, message, Button, Switch, Tooltip } from 'antd';
|
||||
import {
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
Table,
|
||||
DatePicker,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
message,
|
||||
Button,
|
||||
Switch,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import {
|
||||
BarChartOutlined,
|
||||
PieChartOutlined,
|
||||
@@ -82,26 +96,29 @@ function TicketStatistics() {
|
||||
|
||||
const timerRef = useRef(null);
|
||||
|
||||
const fetchStatistics = useCallback(async (isManual = false) => {
|
||||
try {
|
||||
if (isManual) {
|
||||
setLoading(true);
|
||||
}
|
||||
const params = {
|
||||
startDate: dateRange[0].format('YYYY-MM-DD'),
|
||||
endDate: dateRange[1].format('YYYY-MM-DD'),
|
||||
};
|
||||
const fetchStatistics = useCallback(
|
||||
async (isManual = false) => {
|
||||
try {
|
||||
if (isManual) {
|
||||
setLoading(true);
|
||||
}
|
||||
const params = {
|
||||
startDate: dateRange[0].format('YYYY-MM-DD'),
|
||||
endDate: dateRange[1].format('YYYY-MM-DD'),
|
||||
};
|
||||
|
||||
const response = await axios.get('/api/tickets/stats', { params });
|
||||
setStatistics(response.data);
|
||||
setLastUpdateTime(new Date());
|
||||
} catch (error) {
|
||||
message.error('获取统计数据失败');
|
||||
console.error('获取统计数据失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [dateRange]);
|
||||
const response = await axios.get('/api/tickets/stats', { params });
|
||||
setStatistics(response.data);
|
||||
setLastUpdateTime(new Date());
|
||||
} catch (error) {
|
||||
message.error('获取统计数据失败');
|
||||
console.error('获取统计数据失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[dateRange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStatistics();
|
||||
@@ -329,7 +346,13 @@ function TicketStatistics() {
|
||||
title="工单统计报表"
|
||||
extra={
|
||||
<Space>
|
||||
<Tooltip title={lastUpdateTime ? `最后更新: ${dayjs(lastUpdateTime).format('HH:mm:ss')}` : '尚未更新'}>
|
||||
<Tooltip
|
||||
title={
|
||||
lastUpdateTime
|
||||
? `最后更新: ${dayjs(lastUpdateTime).format('HH:mm:ss')}`
|
||||
: '尚未更新'
|
||||
}
|
||||
>
|
||||
<span style={{ fontSize: 12, color: '#888', marginRight: 8 }}>
|
||||
{lastUpdateTime && `更新于 ${dayjs(lastUpdateTime).format('HH:mm:ss')}`}
|
||||
</span>
|
||||
|
||||
@@ -30,7 +30,7 @@ export const getUserFromStorage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
export const formatFileSize = (bytes) => {
|
||||
export const formatFileSize = bytes => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
@@ -42,6 +42,6 @@ export const generateId = (prefix = '') => {
|
||||
return `${prefix}${Date.now()}${Math.random().toString(36).substr(2, 9)}`;
|
||||
};
|
||||
|
||||
export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
export const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
||||
|
||||
@@ -23,15 +23,15 @@ export const TYPE_MAP = {
|
||||
other: '其他设备',
|
||||
};
|
||||
|
||||
export const getStatusConfig = (status) => {
|
||||
export const getStatusConfig = status => {
|
||||
return STATUS_MAP[status] || { text: status, color: 'black', badgeColor: 'default' };
|
||||
};
|
||||
|
||||
export const getTypeLabel = (type) => {
|
||||
export const getTypeLabel = type => {
|
||||
return TYPE_MAP[type] || type;
|
||||
};
|
||||
|
||||
export const getDeviceTypeIcon = (type) => {
|
||||
export const getDeviceTypeIcon = type => {
|
||||
const iconMap = {
|
||||
server: <CloudServerOutlined style={{ color: '#1890ff' }} />,
|
||||
switch: <SwapOutlined style={{ color: '#52c41a' }} />,
|
||||
@@ -80,7 +80,7 @@ export const FIXED_FIELDS = [
|
||||
|
||||
export const SYSTEM_FIELDS = ['createdAt', 'updatedAt', 'Rack', 'Room', 'customFields'];
|
||||
|
||||
export const processDeviceData = (device) => {
|
||||
export const processDeviceData = device => {
|
||||
const deviceWithFields = { ...device };
|
||||
if (device.customFields && typeof device.customFields === 'object') {
|
||||
Object.entries(device.customFields).forEach(([fieldName, value]) => {
|
||||
@@ -98,7 +98,7 @@ export const prepareDeviceFormData = (values, isEditing) => {
|
||||
customFields: {},
|
||||
};
|
||||
|
||||
Object.keys(deviceData).forEach((key) => {
|
||||
Object.keys(deviceData).forEach(key => {
|
||||
if (!FIXED_FIELDS.includes(key) && key !== 'customFields' && key !== 'roomId') {
|
||||
deviceData.customFields[key] = deviceData[key];
|
||||
delete deviceData[key];
|
||||
@@ -116,7 +116,7 @@ export const getFormInitialValues = (device, racks) => {
|
||||
const deviceData = { ...device };
|
||||
const cleanDeviceData = {};
|
||||
|
||||
FIXED_FIELDS.forEach((field) => {
|
||||
FIXED_FIELDS.forEach(field => {
|
||||
if (deviceData[field] !== undefined) {
|
||||
cleanDeviceData[field] = deviceData[field];
|
||||
}
|
||||
@@ -141,7 +141,7 @@ export const getFormInitialValues = (device, racks) => {
|
||||
}
|
||||
|
||||
if (device.rackId) {
|
||||
const rack = racks.find((r) => r.rackId === device.rackId);
|
||||
const rack = racks.find(r => r.rackId === device.rackId);
|
||||
if (rack) {
|
||||
cleanDeviceData.roomId = rack.roomId;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user