feat(3d): 优化3D场景性能并添加纹理缓存

refactor(DeviceModel): 使用相机Z坐标阈值减少背板渲染状态更新
refactor(LODManager): 基于距离变化率优化LOD切换逻辑
feat(TextureCache): 新增纹理缓存类用于U位标签纹理管理
refactor(RackModel): 使用纹理缓存重构U位标签生成逻辑

feat(backend): 添加端口兼容性检查功能
feat(cables): 实现端口类型和速率兼容性验证API

feat(frontend): 新增设备筛选组件FilterableDeviceSelect
refactor(PortPanel): 优化端口占用状态显示和交互
feat(CableWizardModal): 集成端口兼容性检查并优化UI
This commit is contained in:
zhang1106
2026-04-07 17:05:19 +08:00
parent 4f8a44f6b5
commit bfe6ca4744
9 changed files with 1012 additions and 362 deletions
+146 -138
View File
@@ -299,6 +299,20 @@ const PortPanel = ({
</div>
)}
</div>
<div
style={{
marginTop: '12px',
padding: '8px 12px',
background: 'rgba(250,173,20,0.15)',
borderRadius: '6px',
border: '1px solid rgba(250,173,20,0.3)',
fontSize: '12px',
color: '#faad14',
textAlign: 'center',
}}
>
此端口已被占用无法选择
</div>
</>
)}
@@ -419,10 +433,140 @@ const PortPanel = ({
>
{paginatedPorts.map(port => {
const statusColor = getPortStatusColor(port.status);
const isClickable = onPortClick && port.status !== 'disabled';
const isClickable = onPortClick && port.status !== 'disabled' && port.status !== 'occupied';
const cable = findPortCable(port);
const isSelected = selectedPort?.portId === port.portId || selectedPort?.portName === port.portName;
const handlePortClick = () => {
if (isClickable) {
onPortClick(port);
}
};
const portElement = (
<div
onClick={handlePortClick}
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: '6px 4px',
cursor: isClickable ? 'pointer' : 'not-allowed',
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative',
minWidth: '0',
transform: isSelected ? 'scale(1.08)' : 'scale(1)',
boxShadow: isSelected
? '0 0 0 4px rgba(24,144,255,0.15), 0 8px 25px rgba(24,144,255,0.25)'
: isClickable
? '0 0 0 0 rgba(24,144,255,0)'
: 'none',
}}
>
{isSelected && (
<div
style={{
position: 'absolute',
top: '-4px',
right: '-4px',
width: '24px',
height: '24px',
borderRadius: '50%',
background: 'linear-gradient(135deg, #1890ff 0%, #096dd9 100%)',
border: '3px solid #fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 20,
boxShadow: '0 4px 12px rgba(24,144,255,0.4), 0 2px 6px rgba(0,0,0,0.15)',
}}
>
<CheckCircleOutlined
style={{
color: '#fff',
fontSize: '14px',
fontWeight: 'bold',
}}
/>
</div>
)}
<div
style={{
width: isSelected ? '8px' : '6px',
height: isSelected ? '8px' : '6px',
borderRadius: '50%',
background: isSelected ? '#1890ff' : statusColor,
boxShadow: isSelected
? `0 0 8px #1890ff, 0 0 16px #1890ff50`
: `0 0 6px ${statusColor}, 0 0 12px ${statusColor}50`,
marginBottom: '6px',
transition: 'all 0.2s ease',
animation: port.status === 'fault' ? 'pulse 1.5s infinite' : 'none',
}}
/>
<div
style={{
width: '100%',
aspectRatio: '1 / 1.2',
background: isSelected
? 'linear-gradient(180deg, #e6f7ff 0%, #bae7ff 100%)'
: 'linear-gradient(180deg, #2a3441 0%, #1e2530 100%)',
border: `2px solid ${isSelected ? '#1890ff' : statusColor}`,
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
boxShadow: isSelected
? 'inset 0 2px 4px rgba(24,144,255,0.2), 0 4px 12px rgba(24,144,255,0.15)'
: 'inset 0 1px 0 rgba(255,255,255,0.1), 0 2px 4px rgba(0,0,0,0.3)',
transition: 'all 0.2s ease',
}}
>
<div
style={{
fontSize: isSelected ? '12px' : '10px',
color: isSelected ? '#1890ff' : statusColor,
opacity: 0.9,
transition: 'all 0.2s ease',
}}
>
{getPortTypeIcon(port.portType)}
</div>
{cable && (
<div
style={{
position: 'absolute',
top: '1px',
right: '1px',
width: '4px',
height: '4px',
borderRadius: '50%',
background: getCableTypeColor(cable.cableType),
boxShadow: `0 0 3px ${getCableTypeColor(cable.cableType)}`,
}}
/>
)}
</div>
<div
style={{
fontSize: isSelected ? '11px' : '9px',
fontWeight: isSelected ? 600 : 500,
color: isSelected ? '#1890ff' : 'rgba(255, 255, 255, 0.7)',
textAlign: 'center',
marginTop: '4px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '100%',
transition: 'all 0.2s ease',
}}
>
{getPortDisplayName(port.portName)}
</div>
</div>
);
return (
<Tooltip
key={port.portId}
@@ -434,143 +578,7 @@ const PortPanel = ({
border: '1px solid rgba(255, 255, 255, 0.1)',
}}
>
<div
onClick={() => {
console.log('Port clicked:', port);
if (isClickable) {
console.log('Port is clickable, calling onPortClick');
onPortClick(port);
}
}}
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: '6px 4px',
cursor: isClickable ? 'pointer' : 'not-allowed',
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative',
minWidth: '0',
pointerEvents: isClickable ? 'auto' : 'none',
transform: isSelected ? 'scale(1.08)' : 'scale(1)',
boxShadow: isSelected
? '0 0 0 4px rgba(24,144,255,0.15), 0 8px 25px rgba(24,144,255,0.25)'
: isClickable
? '0 0 0 0 rgba(24,144,255,0)'
: 'none',
}}
>
{/* 选中状态标记 - 更明显的视觉反馈 */}
{isSelected && (
<div
style={{
position: 'absolute',
top: '-4px',
right: '-4px',
width: '24px',
height: '24px',
borderRadius: '50%',
background: 'linear-gradient(135deg, #1890ff 0%, #096dd9 100%)',
border: '3px solid #fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 20,
boxShadow: '0 4px 12px rgba(24,144,255,0.4), 0 2px 6px rgba(0,0,0,0.15)',
}}
>
<CheckCircleOutlined
style={{
color: '#fff',
fontSize: '14px',
fontWeight: 'bold',
}}
/>
</div>
)}
{/* LED 指示灯 - 在端口上方 */}
<div
style={{
width: isSelected ? '8px' : '6px',
height: isSelected ? '8px' : '6px',
borderRadius: '50%',
background: isSelected ? '#1890ff' : statusColor,
boxShadow: isSelected
? `0 0 8px #1890ff, 0 0 16px #1890ff50`
: `0 0 6px ${statusColor}, 0 0 12px ${statusColor}50`,
marginBottom: '6px',
transition: 'all 0.2s ease',
animation: port.status === 'fault' ? 'pulse 1.5s infinite' : 'none',
}}
/>
{/* 端口主体 - 矩形样式 */}
<div
style={{
width: '100%',
aspectRatio: '1 / 1.2',
background: isSelected
? 'linear-gradient(180deg, #e6f7ff 0%, #bae7ff 100%)'
: 'linear-gradient(180deg, #2a3441 0%, #1e2530 100%)',
border: `2px solid ${isSelected ? '#1890ff' : statusColor}`,
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
boxShadow: isSelected
? 'inset 0 2px 4px rgba(24,144,255,0.2), 0 4px 12px rgba(24,144,255,0.15)'
: 'inset 0 1px 0 rgba(255,255,255,0.1), 0 2px 4px rgba(0,0,0,0.3)',
transition: 'all 0.2s ease',
}}
>
{/* 端口内部图标 */}
<div
style={{
fontSize: isSelected ? '12px' : '10px',
color: isSelected ? '#1890ff' : statusColor,
opacity: 0.9,
transition: 'all 0.2s ease',
}}
>
{getPortTypeIcon(port.portType)}
</div>
{/* 接线指示标记 */}
{cable && (
<div
style={{
position: 'absolute',
top: '1px',
right: '1px',
width: '4px',
height: '4px',
borderRadius: '50%',
background: getCableTypeColor(cable.cableType),
boxShadow: `0 0 3px ${getCableTypeColor(cable.cableType)}`,
}}
/>
)}
</div>
{/* 端口名称 - 在端口下方 */}
<div
style={{
fontSize: isSelected ? '11px' : '9px',
fontWeight: isSelected ? 600 : 500,
color: isSelected ? '#1890ff' : 'rgba(255, 255, 255, 0.7)',
textAlign: 'center',
marginTop: '4px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '100%',
transition: 'all 0.2s ease',
}}
>
{getPortDisplayName(port.portName)}
</div>
</div>
{portElement}
</Tooltip>
);
})}