2026-03-25 12:14:00 +08:00
|
|
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
|
|
|
import { Button, Tooltip, Dropdown } from 'antd';
|
|
|
|
|
import {
|
|
|
|
|
ArrowLeftOutlined,
|
|
|
|
|
CloudServerOutlined,
|
|
|
|
|
ReloadOutlined,
|
|
|
|
|
EyeOutlined,
|
|
|
|
|
SettingOutlined,
|
|
|
|
|
FullscreenOutlined,
|
|
|
|
|
MenuOutlined,
|
|
|
|
|
SwapOutlined,
|
|
|
|
|
} from '@ant-design/icons';
|
|
|
|
|
import CascadingRackPanel from './CascadingRackPanel';
|
|
|
|
|
import { useResponsiveLayout } from '../../hooks/useResponsiveLayout';
|
|
|
|
|
|
|
|
|
|
const ACTION_BUTTONS_CONFIG = [
|
|
|
|
|
{
|
|
|
|
|
key: 'refresh',
|
|
|
|
|
icon: <ReloadOutlined />,
|
|
|
|
|
label: '刷新',
|
|
|
|
|
tooltip: '刷新数据',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'resetView',
|
|
|
|
|
icon: <EyeOutlined />,
|
|
|
|
|
label: '重置视角',
|
|
|
|
|
tooltip: '重置3D视图',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'config',
|
|
|
|
|
icon: <SettingOutlined />,
|
|
|
|
|
label: '显示配置',
|
|
|
|
|
tooltip: '配置显示字段',
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const RackSelectorHeader = ({
|
|
|
|
|
rooms,
|
|
|
|
|
selectedRoomKey,
|
|
|
|
|
selectedRack,
|
|
|
|
|
onRackSelect,
|
|
|
|
|
onPrevRack,
|
|
|
|
|
onNextRack,
|
|
|
|
|
racksInSelectedRoom,
|
|
|
|
|
deviceSlideEnabled,
|
|
|
|
|
onDeviceSlideToggle,
|
|
|
|
|
onRefresh,
|
|
|
|
|
onResetView,
|
|
|
|
|
onOpenConfig,
|
|
|
|
|
onBack,
|
|
|
|
|
}) => {
|
|
|
|
|
const [selectorVisible, setSelectorVisible] = useState(false);
|
|
|
|
|
const selectorRef = useRef(null);
|
|
|
|
|
const { screenSize, config, isMobile } = useResponsiveLayout();
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-03-27 19:12:16 +08:00
|
|
|
const handleClickOutside = event => {
|
2026-03-25 12:14:00 +08:00
|
|
|
if (selectorRef.current && !selectorRef.current.contains(event.target)) {
|
|
|
|
|
setSelectorVisible(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (selectorVisible) {
|
|
|
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
|
|
|
};
|
|
|
|
|
}, [selectorVisible]);
|
|
|
|
|
|
|
|
|
|
const handleRackSelect = useCallback(
|
|
|
|
|
(rack, room) => {
|
|
|
|
|
onRackSelect(rack, room);
|
|
|
|
|
setSelectorVisible(false);
|
|
|
|
|
},
|
|
|
|
|
[onRackSelect]
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
const handleKeyDown = useCallback(e => {
|
|
|
|
|
if (e.key === 'Escape') {
|
|
|
|
|
setSelectorVisible(false);
|
|
|
|
|
}
|
|
|
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setSelectorVisible(prev => !prev);
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
2026-03-25 12:14:00 +08:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
|
|
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
|
|
|
}, [handleKeyDown]);
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
const selectedRoom = rooms.find(r => r.key === selectedRoomKey);
|
2026-03-25 12:14:00 +08:00
|
|
|
const displayText = selectedRack
|
|
|
|
|
? `${selectedRoom?.name || ''} / ${selectedRack.name}`
|
|
|
|
|
: '选择机房 / 机柜';
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
const canNavigatePrev = racksInSelectedRoom && racksInSelectedRoom.length > 1 && selectedRack;
|
|
|
|
|
const canNavigateNext = racksInSelectedRoom && racksInSelectedRoom.length > 1 && selectedRack;
|
2026-03-25 12:14:00 +08:00
|
|
|
|
|
|
|
|
const getCurrentRackIndex = () => {
|
|
|
|
|
if (!selectedRack || !racksInSelectedRoom) return -1;
|
2026-03-27 19:12:16 +08:00
|
|
|
return racksInSelectedRoom.findIndex(r => r.rackId === selectedRack.rackId);
|
2026-03-25 12:14:00 +08:00
|
|
|
};
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
const dropdownMenuItems = ACTION_BUTTONS_CONFIG.map(btn => ({
|
2026-03-25 12:14:00 +08:00
|
|
|
key: btn.key,
|
|
|
|
|
label: (
|
|
|
|
|
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
|
|
|
{btn.icon}
|
|
|
|
|
{btn.label}
|
|
|
|
|
</span>
|
|
|
|
|
),
|
|
|
|
|
onClick: () => {
|
|
|
|
|
switch (btn.key) {
|
|
|
|
|
case 'refresh':
|
|
|
|
|
onRefresh();
|
|
|
|
|
break;
|
|
|
|
|
case 'resetView':
|
|
|
|
|
onResetView();
|
|
|
|
|
break;
|
|
|
|
|
case 'config':
|
|
|
|
|
onOpenConfig();
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const renderLeftSection = () => (
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
flexShrink: 0,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Button
|
|
|
|
|
type="text"
|
|
|
|
|
icon={<ArrowLeftOutlined style={{ color: 'rgba(255,255,255,0.8)' }} />}
|
|
|
|
|
onClick={onBack}
|
|
|
|
|
className="header-icon-btn"
|
|
|
|
|
style={{ marginRight: 8 }}
|
|
|
|
|
/>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
background: 'rgba(255,255,255,0.05)',
|
|
|
|
|
padding: '6px 12px',
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
border: '1px solid rgba(255,255,255,0.05)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<CloudServerOutlined style={{ fontSize: 18, color: '#3b82f6', marginRight: 10 }} />
|
|
|
|
|
<span
|
|
|
|
|
style={{
|
|
|
|
|
fontSize: 15,
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
color: '#f8fafc',
|
|
|
|
|
letterSpacing: '0.3px',
|
|
|
|
|
whiteSpace: 'nowrap',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
3D 机柜可视化
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const renderSelector = () => {
|
|
|
|
|
const panelWidth = isMobile ? '100%' : 520;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={selectorRef}
|
|
|
|
|
style={{
|
|
|
|
|
position: 'relative',
|
|
|
|
|
flex: config.panelFullWidth ? 1 : '0 1 auto',
|
|
|
|
|
maxWidth: config.panelFullWidth ? 'none' : 560,
|
|
|
|
|
margin: '0 16px',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div
|
2026-03-27 19:12:16 +08:00
|
|
|
onClick={() => setSelectorVisible(prev => !prev)}
|
2026-03-25 12:14:00 +08:00
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
2026-03-27 19:12:16 +08:00
|
|
|
background: selectorVisible ? 'rgba(59, 130, 246, 0.15)' : 'rgba(255, 255, 255, 0.08)',
|
2026-03-25 12:14:00 +08:00
|
|
|
border: selectorVisible
|
|
|
|
|
? '1px solid rgba(59, 130, 246, 0.5)'
|
|
|
|
|
: '1px solid rgba(255, 255, 255, 0.12)',
|
|
|
|
|
borderRadius: 10,
|
|
|
|
|
padding: '8px 14px',
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
transition: 'all 0.2s ease',
|
|
|
|
|
minHeight: 40,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
flex: 1,
|
|
|
|
|
minWidth: 0,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<SwapOutlined
|
|
|
|
|
style={{
|
|
|
|
|
color: selectorVisible ? '#60a5fa' : 'rgba(255,255,255,0.5)',
|
|
|
|
|
marginRight: 10,
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
transition: 'color 0.2s',
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<span
|
|
|
|
|
style={{
|
|
|
|
|
color: selectedRack ? '#f8fafc' : 'rgba(255,255,255,0.5)',
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
fontWeight: selectedRack ? 500 : 400,
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
textOverflow: 'ellipsis',
|
|
|
|
|
whiteSpace: 'nowrap',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{displayText}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
gap: 6,
|
|
|
|
|
marginLeft: 12,
|
|
|
|
|
flexShrink: 0,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{canNavigatePrev && (
|
|
|
|
|
<div
|
2026-03-27 19:12:16 +08:00
|
|
|
onClick={e => {
|
2026-03-25 12:14:00 +08:00
|
|
|
e.stopPropagation();
|
|
|
|
|
onPrevRack();
|
|
|
|
|
}}
|
|
|
|
|
style={{
|
|
|
|
|
padding: '4px 8px',
|
|
|
|
|
borderRadius: 6,
|
|
|
|
|
background: 'rgba(255,255,255,0.08)',
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
color: 'rgba(255,255,255,0.7)',
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
transition: 'all 0.15s',
|
|
|
|
|
}}
|
2026-03-27 19:12:16 +08:00
|
|
|
onMouseEnter={e => {
|
2026-03-25 12:14:00 +08:00
|
|
|
e.currentTarget.style.background = 'rgba(255,255,255,0.15)';
|
|
|
|
|
}}
|
2026-03-27 19:12:16 +08:00
|
|
|
onMouseLeave={e => {
|
2026-03-25 12:14:00 +08:00
|
|
|
e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
←
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{canNavigateNext && (
|
|
|
|
|
<div
|
2026-03-27 19:12:16 +08:00
|
|
|
onClick={e => {
|
2026-03-25 12:14:00 +08:00
|
|
|
e.stopPropagation();
|
|
|
|
|
onNextRack();
|
|
|
|
|
}}
|
|
|
|
|
style={{
|
|
|
|
|
padding: '4px 8px',
|
|
|
|
|
borderRadius: 6,
|
|
|
|
|
background: 'rgba(255,255,255,0.08)',
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
color: 'rgba(255,255,255,0.7)',
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
transition: 'all 0.15s',
|
|
|
|
|
}}
|
2026-03-27 19:12:16 +08:00
|
|
|
onMouseEnter={e => {
|
2026-03-25 12:14:00 +08:00
|
|
|
e.currentTarget.style.background = 'rgba(255,255,255,0.15)';
|
|
|
|
|
}}
|
2026-03-27 19:12:16 +08:00
|
|
|
onMouseLeave={e => {
|
2026-03-25 12:14:00 +08:00
|
|
|
e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
→
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<span
|
|
|
|
|
style={{
|
|
|
|
|
color: 'rgba(255,255,255,0.5)',
|
|
|
|
|
fontSize: 10,
|
|
|
|
|
transition: 'transform 0.2s',
|
|
|
|
|
transform: selectorVisible ? 'rotate(180deg)' : 'rotate(0deg)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
▼
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<CascadingRackPanel
|
|
|
|
|
rooms={rooms}
|
|
|
|
|
selectedRoomKey={selectedRoomKey}
|
|
|
|
|
selectedRackId={selectedRack?.rackId}
|
|
|
|
|
onSelect={handleRackSelect}
|
|
|
|
|
visible={selectorVisible}
|
|
|
|
|
onClose={() => setSelectorVisible(false)}
|
|
|
|
|
triggerRef={selectorRef}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderActionButton = (btn, index) => {
|
|
|
|
|
if (isMobile && btn.key === 'config') {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Tooltip key={btn.key} title={btn.tooltip} placement="bottom">
|
|
|
|
|
<Button
|
|
|
|
|
type="primary"
|
|
|
|
|
ghost
|
|
|
|
|
icon={btn.icon}
|
|
|
|
|
onClick={btn.onClick}
|
|
|
|
|
className="header-action-btn"
|
|
|
|
|
style={{
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
borderColor: 'rgba(255,255,255,0.25)',
|
|
|
|
|
color: 'rgba(255,255,255,0.85)',
|
|
|
|
|
height: 36,
|
|
|
|
|
padding: '0 12px',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{!isMobile && !config.buttonIconOnly && btn.label}
|
|
|
|
|
</Button>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderDeviceSlideToggle = () => {
|
|
|
|
|
if (!config.showDeviceSlide) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
background: 'rgba(255,255,255,0.05)',
|
|
|
|
|
padding: '4px 10px',
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
border: '1px solid rgba(255,255,255,0.05)',
|
|
|
|
|
gap: 6,
|
|
|
|
|
height: 36,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<FullscreenOutlined
|
|
|
|
|
style={{
|
|
|
|
|
color: deviceSlideEnabled ? '#22c55e' : 'rgba(255,255,255,0.4)',
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
{!isMobile && (
|
|
|
|
|
<span
|
|
|
|
|
style={{
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
color: deviceSlideEnabled ? 'rgba(255,255,255,0.9)' : 'rgba(255,255,255,0.5)',
|
|
|
|
|
whiteSpace: 'nowrap',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
设备弹出
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
<div
|
|
|
|
|
onClick={() => onDeviceSlideToggle(!deviceSlideEnabled)}
|
|
|
|
|
style={{
|
|
|
|
|
width: 32,
|
|
|
|
|
height: 18,
|
|
|
|
|
borderRadius: 9,
|
|
|
|
|
background: deviceSlideEnabled ? '#22c55e' : 'rgba(255,255,255,0.2)',
|
|
|
|
|
position: 'relative',
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
transition: 'background 0.2s',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
width: 14,
|
|
|
|
|
height: 14,
|
|
|
|
|
borderRadius: '50%',
|
|
|
|
|
background: 'white',
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
top: 2,
|
|
|
|
|
left: deviceSlideEnabled ? 16 : 2,
|
|
|
|
|
transition: 'left 0.2s',
|
|
|
|
|
boxShadow: '0 1px 3px rgba(0,0,0,0.3)',
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderRightSection = () => {
|
|
|
|
|
if (config.collapseActions) {
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0 }}>
|
|
|
|
|
{renderDeviceSlideToggle()}
|
2026-03-27 19:12:16 +08:00
|
|
|
<Dropdown menu={{ items: dropdownMenuItems }} trigger={['click']} placement="bottomRight">
|
2026-03-25 12:14:00 +08:00
|
|
|
<Button
|
|
|
|
|
type="text"
|
|
|
|
|
icon={<MenuOutlined style={{ color: 'rgba(255,255,255,0.8)' }} />}
|
|
|
|
|
className="header-icon-btn"
|
|
|
|
|
style={{ borderRadius: 8 }}
|
|
|
|
|
/>
|
|
|
|
|
</Dropdown>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
gap: 8,
|
|
|
|
|
flexShrink: 0,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{renderDeviceSlideToggle()}
|
|
|
|
|
{ACTION_BUTTONS_CONFIG.map(renderActionButton)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
height: 64,
|
|
|
|
|
padding: '0 20px 0 12px',
|
|
|
|
|
marginLeft: -24,
|
|
|
|
|
background: 'rgba(15, 23, 42, 0.6)',
|
|
|
|
|
backdropFilter: 'blur(20px)',
|
|
|
|
|
borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
|
|
|
|
|
flexShrink: 0,
|
|
|
|
|
position: 'relative',
|
|
|
|
|
zIndex: 100,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<style>{`
|
|
|
|
|
.header-icon-btn:hover {
|
|
|
|
|
color: white !important;
|
|
|
|
|
background: rgba(255,255,255,0.1) !important;
|
|
|
|
|
}
|
|
|
|
|
.header-action-btn:hover {
|
|
|
|
|
color: white !important;
|
|
|
|
|
border-color: rgba(255,255,255,0.5) !important;
|
|
|
|
|
background: rgba(255,255,255,0.1) !important;
|
|
|
|
|
}
|
|
|
|
|
`}</style>
|
|
|
|
|
|
|
|
|
|
{renderLeftSection()}
|
|
|
|
|
{renderSelector()}
|
|
|
|
|
{renderRightSection()}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
export default RackSelectorHeader;
|