优化了机柜可视化页面,添加背景图片等功能,显示位置等
This commit is contained in:
@@ -23,13 +23,57 @@ style.textContent = `
|
|||||||
@keyframes slideIn {
|
@keyframes slideIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-50%) translateX(20px);
|
transform: translateY(-50%) translateX(20px) scale(0.95);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(-50%) translateX(0);
|
transform: translateY(-50%) translateX(0) scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 强制设备详情面板在最顶层,超越所有3D空间 */
|
||||||
|
.device-detail-panel {
|
||||||
|
z-index: 2147483647 !important; /* 使用最大z-index值 */
|
||||||
|
position: fixed !important;
|
||||||
|
transform: translateY(-50%) !important; /* 强制2D变换 */
|
||||||
|
backface-visibility: hidden !important;
|
||||||
|
-webkit-backface-visibility: hidden !important;
|
||||||
|
perspective: none !important;
|
||||||
|
transform-style: flat !important;
|
||||||
|
isolation: isolate !important;
|
||||||
|
will-change: transform !important;
|
||||||
|
contain: layout style paint !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
/* 强制独立层 */
|
||||||
|
-webkit-transform: translateY(-50%) !important;
|
||||||
|
-moz-transform: translateY(-50%) !important;
|
||||||
|
-ms-transform: translateY(-50%) !important;
|
||||||
|
-o-transform: translateY(-50%) !important;
|
||||||
|
/* 强制最高优先级 */
|
||||||
|
filter: drop-shadow(0 0 0 transparent) !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
max-width: none !important;
|
||||||
|
min-width: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 防止页面3D空间影响设备详情面板 */
|
||||||
|
.rack-visualization-container {
|
||||||
|
perspective: none !important;
|
||||||
|
transform-style: flat !important;
|
||||||
|
contain: layout !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保页面所有元素都不会覆盖设备详情面板 */
|
||||||
|
html, body {
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 强制提升页面所有容器的层级 */
|
||||||
|
.ant-layout, .ant-card, .ant-card-head, .ant-card-body {
|
||||||
|
contain: none !important;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
|
|
||||||
@@ -98,6 +142,9 @@ function RackVisualization() {
|
|||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [hoveredDevice, setHoveredDevice] = useState(null);
|
const [hoveredDevice, setHoveredDevice] = useState(null);
|
||||||
const [hoverTimeout, setHoverTimeout] = useState(null);
|
const [hoverTimeout, setHoverTimeout] = useState(null);
|
||||||
|
const [backgroundImage, setBackgroundImage] = useState(null);
|
||||||
|
const [backgroundType, setBackgroundType] = useState('gradient'); // gradient or image
|
||||||
|
const [backgroundSize, setBackgroundSize] = useState('contain'); // cover, contain, auto
|
||||||
|
|
||||||
// 获取所有机柜
|
// 获取所有机柜
|
||||||
const fetchRacks = async () => {
|
const fetchRacks = async () => {
|
||||||
@@ -416,107 +463,93 @@ function RackVisualization() {
|
|||||||
return marks;
|
return marks;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 生成左侧U数标记(从实际范围开始显示)
|
// 生成左侧U数标记(从U1开始显示)
|
||||||
const generateLeftUMarks = (height) => {
|
const generateLeftUMarks = (height) => {
|
||||||
const marks = [];
|
const marks = [];
|
||||||
const uHeight = 18;
|
const uHeight = 18;
|
||||||
|
const rackContentHeight = height * uHeight; // 机柜内容总高度
|
||||||
|
const containerPadding = 6; // 前面板上下padding总和
|
||||||
|
|
||||||
// 从实际U位范围开始(通常是4-42U或类似范围)
|
for (let u = 1; u <= height; u++) {
|
||||||
const startU = Math.max(1, height - 40); // 假设机柜有40U左右,从较高位置开始显示
|
// 计算从上到下的位置,考虑前面板的padding
|
||||||
|
// U1在最底部,所以从底部开始计算
|
||||||
for (let i = 1; i <= height; i++) {
|
const bottomToTop = (u - 1) * uHeight;
|
||||||
const actualU = startU + i - 1;
|
const topPosition = rackContentHeight - bottomToTop - uHeight + containerPadding / 2;
|
||||||
if (actualU <= 42) { // 只显示到42U
|
|
||||||
marks.push(
|
marks.push(
|
||||||
<div key={`left-${i}`}>
|
<div key={`left-${u}`}>
|
||||||
{/* U位网格背景 */}
|
{/* U位序号 */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: `${(height - i) * uHeight}px`,
|
top: `${topPosition}px`,
|
||||||
left: '0',
|
left: '0',
|
||||||
width: '25px',
|
width: '30px',
|
||||||
height: `${uHeight}px`,
|
height: `${uHeight}px`,
|
||||||
backgroundColor: i % 2 === 0 ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.05)',
|
display: 'flex',
|
||||||
borderRight: '1px solid #555'
|
alignItems: 'center',
|
||||||
}}
|
justifyContent: 'center',
|
||||||
/>
|
color: u % 5 === 0 ? '#ff4757' : '#ff9500', // 亮红色和亮橙色
|
||||||
{/* U位序号 */}
|
fontSize: '11px',
|
||||||
<div
|
fontWeight: u % 5 === 0 ? 'bold' : 'normal',
|
||||||
style={{
|
textShadow: '1px 1px 2px rgba(0,0,0,0.9), 0 0 4px rgba(255,255,255,0.3)',
|
||||||
position: 'absolute',
|
zIndex: 30, // 提高z-index确保在前面板之上
|
||||||
top: `${(height - i) * uHeight + 1}px`,
|
backgroundColor: 'rgba(0,0,0,0.8)',
|
||||||
left: '0',
|
borderRadius: '3px',
|
||||||
width: '25px',
|
border: '1px solid rgba(255,255,255,0.3)',
|
||||||
height: '100%',
|
boxShadow: '0 2px 4px rgba(0,0,0,0.3)'
|
||||||
display: 'flex',
|
}}
|
||||||
alignItems: 'center',
|
>
|
||||||
justifyContent: 'center',
|
{u}
|
||||||
color: i % 5 === 0 ? '#00ff88' : '#ccc',
|
|
||||||
fontSize: '10px',
|
|
||||||
fontWeight: i % 5 === 0 ? 'bold' : 'normal',
|
|
||||||
textShadow: '1px 1px 1px rgba(0,0,0,0.8)',
|
|
||||||
zIndex: 2
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{actualU}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return marks;
|
return marks;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 生成右侧U数标记(从实际范围开始显示)
|
// 生成右侧U数标记(从U1开始显示)
|
||||||
const generateRightUMarks = (height) => {
|
const generateRightUMarks = (height) => {
|
||||||
const marks = [];
|
const marks = [];
|
||||||
const uHeight = 18;
|
const uHeight = 18;
|
||||||
|
const rackContentHeight = height * uHeight; // 机柜内容总高度
|
||||||
|
const containerPadding = 6; // 前面板上下padding总和
|
||||||
|
|
||||||
// 从实际U位范围开始(通常是4-42U或类似范围)
|
for (let u = 1; u <= height; u++) {
|
||||||
const startU = Math.max(1, height - 40); // 假设机柜有40U左右,从较高位置开始显示
|
// 计算从上到下的位置,考虑前面板的padding
|
||||||
|
// U1在最底部,所以从底部开始计算
|
||||||
for (let i = 1; i <= height; i++) {
|
const bottomToTop = (u - 1) * uHeight;
|
||||||
const actualU = startU + i - 1;
|
const topPosition = rackContentHeight - bottomToTop - uHeight + containerPadding / 2;
|
||||||
if (actualU <= 42) { // 只显示到42U
|
|
||||||
marks.push(
|
marks.push(
|
||||||
<div key={`right-${i}`}>
|
<div key={`right-${u}`}>
|
||||||
{/* U位网格背景 */}
|
{/* U位序号 */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: `${(height - i) * uHeight}px`,
|
top: `${topPosition}px`,
|
||||||
right: '0',
|
right: '0',
|
||||||
width: '25px',
|
width: '30px',
|
||||||
height: `${uHeight}px`,
|
height: `${uHeight}px`,
|
||||||
backgroundColor: i % 2 === 0 ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.05)',
|
display: 'flex',
|
||||||
borderLeft: '1px solid #555'
|
alignItems: 'center',
|
||||||
}}
|
justifyContent: 'center',
|
||||||
/>
|
color: u % 5 === 0 ? '#ff4757' : '#ff9500', // 亮红色和亮橙色
|
||||||
{/* U位序号 */}
|
fontSize: '11px',
|
||||||
<div
|
fontWeight: u % 5 === 0 ? 'bold' : 'normal',
|
||||||
style={{
|
textShadow: '1px 1px 2px rgba(0,0,0,0.9), 0 0 4px rgba(255,255,255,0.3)',
|
||||||
position: 'absolute',
|
zIndex: 30, // 提高z-index确保在前面板之上
|
||||||
top: `${(height - i) * uHeight + 1}px`,
|
backgroundColor: 'rgba(0,0,0,0.8)',
|
||||||
right: '0',
|
borderRadius: '3px',
|
||||||
width: '25px',
|
border: '1px solid rgba(255,255,255,0.3)',
|
||||||
height: '100%',
|
boxShadow: '0 2px 4px rgba(0,0,0,0.3)'
|
||||||
display: 'flex',
|
}}
|
||||||
alignItems: 'center',
|
>
|
||||||
justifyContent: 'center',
|
{u}
|
||||||
color: i % 5 === 0 ? '#00ff88' : '#ccc',
|
|
||||||
fontSize: '10px',
|
|
||||||
fontWeight: i % 5 === 0 ? 'bold' : 'normal',
|
|
||||||
textShadow: '1px 1px 1px rgba(0,0,0,0.8)',
|
|
||||||
zIndex: 2
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{actualU}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return marks;
|
return marks;
|
||||||
@@ -655,6 +688,59 @@ function RackVisualization() {
|
|||||||
<Button icon={<ZoomInOutlined />} onClick={handleZoomIn}>放大</Button>
|
<Button icon={<ZoomInOutlined />} onClick={handleZoomIn}>放大</Button>
|
||||||
<Button icon={<ZoomOutOutlined />} onClick={handleZoomOut}>缩小</Button>
|
<Button icon={<ZoomOutOutlined />} onClick={handleZoomOut}>缩小</Button>
|
||||||
<Button icon={<RotateRightOutlined />} onClick={handleResetView}>重置视角</Button>
|
<Button icon={<RotateRightOutlined />} onClick={handleResetView}>重置视角</Button>
|
||||||
|
<Select
|
||||||
|
placeholder="选择背景类型"
|
||||||
|
style={{ width: 150 }}
|
||||||
|
value={backgroundType}
|
||||||
|
onChange={setBackgroundType}
|
||||||
|
>
|
||||||
|
<Option value="gradient">渐变背景</Option>
|
||||||
|
<Option value="image">自定义图片</Option>
|
||||||
|
</Select>
|
||||||
|
{backgroundType === 'image' && (
|
||||||
|
<Space>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="输入图片URL"
|
||||||
|
style={{ width: 200, padding: '4px 8px', borderRadius: '4px', border: '1px solid #d9d9d9' }}
|
||||||
|
onChange={(e) => setBackgroundImage(e.target.value)}
|
||||||
|
value={backgroundImage || ''}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={(e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
setBackgroundImage(event.target.result);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
id="backgroundFileInput"
|
||||||
|
/>
|
||||||
|
<Button onClick={() => document.getElementById('backgroundFileInput').click()}>上传图片</Button>
|
||||||
|
<Select
|
||||||
|
placeholder="图片大小"
|
||||||
|
style={{ width: 100 }}
|
||||||
|
value={backgroundSize}
|
||||||
|
onChange={setBackgroundSize}
|
||||||
|
>
|
||||||
|
<Option value="contain">自适应</Option>
|
||||||
|
<Option value="cover">覆盖</Option>
|
||||||
|
<Option value="auto">原始大小</Option>
|
||||||
|
</Select>
|
||||||
|
{backgroundImage && (
|
||||||
|
<Button onClick={() => {
|
||||||
|
setBackgroundImage(null);
|
||||||
|
setBackgroundType('gradient');
|
||||||
|
}}>清除</Button>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -673,17 +759,32 @@ function RackVisualization() {
|
|||||||
加载机柜数据中...
|
加载机柜数据中...
|
||||||
</div>
|
</div>
|
||||||
) : selectedRack ? (
|
) : selectedRack ? (
|
||||||
<div style={{
|
<div
|
||||||
width: '100%',
|
className="rack-visualization-container"
|
||||||
height: '600px',
|
style={{
|
||||||
display: 'flex',
|
width: '100%',
|
||||||
justifyContent: 'center',
|
height: 'calc(100vh - 200px)',
|
||||||
alignItems: 'center',
|
display: 'flex',
|
||||||
perspective: '1000px',
|
justifyContent: 'center',
|
||||||
overflow: 'auto',
|
alignItems: 'center',
|
||||||
backgroundColor: '#f5f5f5',
|
perspective: '1000px',
|
||||||
position: 'relative' // 为设备详情面板定位
|
overflow: 'hidden',
|
||||||
}}>
|
backgroundColor: '#f0f2f5',
|
||||||
|
background: backgroundType === 'image' && backgroundImage
|
||||||
|
? `url(${backgroundImage})`
|
||||||
|
: 'linear-gradient(135deg, #ff0000 0%, #ff7f00 14%, #ffff00 28%, #00ff00 42%, #0000ff 57%, #4b0082 71%, #8b00ff 100%)',
|
||||||
|
backgroundSize: backgroundType === 'image' ? backgroundSize : 'auto',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
position: 'relative',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #e8e8e8',
|
||||||
|
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
padding: '20px',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
flexShrink: 0
|
||||||
|
}}>
|
||||||
{loadingDevices && (
|
{loadingDevices && (
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -707,7 +808,10 @@ function RackVisualization() {
|
|||||||
style={{
|
style={{
|
||||||
transform: `scale(${zoom}) rotateY(${rotation}deg)`,
|
transform: `scale(${zoom}) rotateY(${rotation}deg)`,
|
||||||
transition: 'transform 0.3s ease',
|
transition: 'transform 0.3s ease',
|
||||||
transformStyle: 'preserve-3d'
|
transformStyle: 'preserve-3d',
|
||||||
|
position: 'relative',
|
||||||
|
padding: '40px 50px', // 为U数标记预留空间
|
||||||
|
willChange: 'transform' // 优化性能
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 机柜主体 */}
|
{/* 机柜主体 */}
|
||||||
@@ -720,8 +824,34 @@ function RackVisualization() {
|
|||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
boxShadow: '0 8px 32px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.2)',
|
boxShadow: '0 8px 32px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.2)',
|
||||||
transformStyle: 'preserve-3d',
|
transformStyle: 'preserve-3d',
|
||||||
overflow: 'hidden'
|
overflow: 'visible'
|
||||||
}}>
|
}}>
|
||||||
|
{/* 左侧U数标记 */}
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: '-35px',
|
||||||
|
top: '0',
|
||||||
|
width: '35px',
|
||||||
|
height: '100%',
|
||||||
|
zIndex: 20,
|
||||||
|
pointerEvents: 'none' // 确保不干扰鼠标事件
|
||||||
|
}}>
|
||||||
|
{generateLeftUMarks(selectedRack.height)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右侧U数标记 */}
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: '-35px',
|
||||||
|
top: '0',
|
||||||
|
width: '35px',
|
||||||
|
height: '100%',
|
||||||
|
zIndex: 20,
|
||||||
|
pointerEvents: 'none' // 确保不干扰鼠标事件
|
||||||
|
}}>
|
||||||
|
{generateRightUMarks(selectedRack.height)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 前面板 */}
|
{/* 前面板 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -740,32 +870,7 @@ function RackVisualization() {
|
|||||||
border: '1px solid rgba(255,255,255,0.1)',
|
border: '1px solid rgba(255,255,255,0.1)',
|
||||||
overflow: 'visible'
|
overflow: 'visible'
|
||||||
}}>
|
}}>
|
||||||
{/* 左侧U数标记 */}
|
{/* 移除了内侧U数标记,因为外侧已有U数显示 */}
|
||||||
<div style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: '-25px',
|
|
||||||
top: '0',
|
|
||||||
width: '25px',
|
|
||||||
height: '100%',
|
|
||||||
zIndex: 10
|
|
||||||
}}>
|
|
||||||
{generateLeftUMarks(selectedRack.height)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 右侧U数标记 */}
|
|
||||||
<div style={{
|
|
||||||
position: 'absolute',
|
|
||||||
right: '-25px',
|
|
||||||
top: '0',
|
|
||||||
width: '25px',
|
|
||||||
height: '100%',
|
|
||||||
zIndex: 10
|
|
||||||
}}>
|
|
||||||
{generateRightUMarks(selectedRack.height)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 原有的U数标记(内部) */}
|
|
||||||
{generateUMarks(selectedRack.height)}
|
|
||||||
|
|
||||||
{/* 设备 */}
|
{/* 设备 */}
|
||||||
{/* 调试信息 */}
|
{/* 调试信息 */}
|
||||||
@@ -1058,20 +1163,38 @@ function RackVisualization() {
|
|||||||
{/* 设备详情面板 */}
|
{/* 设备详情面板 */}
|
||||||
{hoveredDevice && (
|
{hoveredDevice && (
|
||||||
<div
|
<div
|
||||||
|
className="device-detail-panel"
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed', // 使用 fixed 定位确保相对于视口
|
position: 'fixed !important', // 强制fixed定位
|
||||||
right: '20px',
|
right: '20px',
|
||||||
top: '50%',
|
top: '50%',
|
||||||
transform: 'translateY(-50%)',
|
transform: 'translateY(-50%) !important', // 强制2D变换
|
||||||
width: '320px',
|
width: '320px',
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
boxShadow: '0 8px 32px rgba(0,0,0,0.3)',
|
boxShadow: '0 8px 32px rgba(0,0,0,0.3)',
|
||||||
padding: '20px',
|
padding: '20px',
|
||||||
border: '1px solid #e8e8e8',
|
border: '1px solid #e8e8e8',
|
||||||
zIndex: 10000, // 提高 z-index 确保在最上层
|
|
||||||
animation: 'slideIn 0.3s ease-out',
|
animation: 'slideIn 0.3s ease-out',
|
||||||
pointerEvents: 'auto' // 确保面板可以接收鼠标事件
|
pointerEvents: 'auto',
|
||||||
|
willChange: 'transform',
|
||||||
|
isolation: 'isolate',
|
||||||
|
backfaceVisibility: 'hidden',
|
||||||
|
// 强制最高优先级
|
||||||
|
zIndex: 2147483647,
|
||||||
|
perspective: 'none',
|
||||||
|
transformStyle: 'flat',
|
||||||
|
contain: 'layout style paint',
|
||||||
|
overflow: 'visible',
|
||||||
|
filter: 'drop-shadow(0 0 0 transparent)',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
maxWidth: 'none',
|
||||||
|
minWidth: 'auto',
|
||||||
|
// 强制独立层
|
||||||
|
WebkitTransform: 'translateY(-50%)',
|
||||||
|
MozTransform: 'translateY(-50%)',
|
||||||
|
msTransform: 'translateY(-50%)',
|
||||||
|
OTransform: 'translateY(-50%)'
|
||||||
}}
|
}}
|
||||||
key={`panel-${hoveredDevice._key || hoveredDevice.deviceId || hoveredDevice.id || 'unknown'}`}
|
key={`panel-${hoveredDevice._key || hoveredDevice.deviceId || hoveredDevice.id || 'unknown'}`}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user