refactor(floorplan): 重构平面图组件与样式
- 移除未使用的编辑模式、热力图、搜索等功能 - 将样式代码抽离到独立样式文件 - 优化工具栏和缩放控制组件实现 - 添加导出图片功能 - 简化上下文状态管理 - 优化设备提示框和空状态显示
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Space, Tooltip } from 'antd';
|
||||
import { ReloadOutlined, FullscreenOutlined, FullscreenExitOutlined } from '@ant-design/icons';
|
||||
import { Button, Space, Tooltip } from 'antd';
|
||||
import { ReloadOutlined, FullscreenOutlined, FullscreenExitOutlined, DownloadOutlined } from '@ant-design/icons';
|
||||
import RoomSelector from './RoomSelector';
|
||||
import ZoomControls from './ZoomControls';
|
||||
import { ToolbarWrapper, ToolbarDivider } from '../styles';
|
||||
|
||||
const FloorPlanToolbar = ({
|
||||
selectedRoomId,
|
||||
@@ -14,31 +15,16 @@ const FloorPlanToolbar = ({
|
||||
onRefresh,
|
||||
isFullscreen,
|
||||
onToggleFullscreen,
|
||||
onExport,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '10px 20px',
|
||||
background: '#ffffff',
|
||||
borderBottom: '1px solid rgba(0,0,0,0.05)',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.03)',
|
||||
flexWrap: 'wrap',
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<ToolbarWrapper>
|
||||
<Space size={14} align="center">
|
||||
<RoomSelector
|
||||
selectedRoomId={selectedRoomId}
|
||||
onRoomChange={onRoomChange}
|
||||
/>
|
||||
<div style={{
|
||||
width: 1,
|
||||
height: 28,
|
||||
background: '#e8e8e8',
|
||||
}} />
|
||||
<ToolbarDivider />
|
||||
<ZoomControls
|
||||
zoom={zoom}
|
||||
onZoomIn={onZoomIn}
|
||||
@@ -48,73 +34,38 @@ const FloorPlanToolbar = ({
|
||||
</Space>
|
||||
|
||||
<Space size={10} align="center">
|
||||
{onExport && (
|
||||
<Tooltip title="导出图片" placement="bottom">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={onExport}
|
||||
style={{ fontSize: 16 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip title={isFullscreen ? '退出全屏' : '全屏查看'} placement="bottom">
|
||||
<button
|
||||
<Button
|
||||
type="text"
|
||||
icon={isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
||||
onClick={onToggleFullscreen}
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#fff',
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: 8,
|
||||
cursor: 'pointer',
|
||||
color: '#595959',
|
||||
transition: 'all 0.2s',
|
||||
fontSize: 16,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.background = '#f8f9fa';
|
||||
e.target.style.borderColor = '#d9d9d9';
|
||||
e.target.style.color = '#262626';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.background = '#fff';
|
||||
e.target.style.borderColor = '#e8e8e8';
|
||||
e.target.style.color = '#595959';
|
||||
}}
|
||||
>
|
||||
{isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
||||
</button>
|
||||
style={{ fontSize: 16 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{onRefresh && (
|
||||
<Tooltip title="刷新数据" placement="bottom">
|
||||
<button
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={onRefresh}
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#fff',
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: 8,
|
||||
cursor: 'pointer',
|
||||
color: '#595959',
|
||||
transition: 'all 0.2s',
|
||||
fontSize: 16,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.background = '#f8f9fa';
|
||||
e.target.style.borderColor = '#d9d9d9';
|
||||
e.target.style.color = '#262626';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.background = '#fff';
|
||||
e.target.style.borderColor = '#e8e8e8';
|
||||
e.target.style.color = '#595959';
|
||||
}}
|
||||
>
|
||||
<ReloadOutlined />
|
||||
</button>
|
||||
style={{ fontSize: 16 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
</ToolbarWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Select, Spin } from 'antd';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Select, Spin, message } from 'antd';
|
||||
import { HomeOutlined } from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import { RoomOptionContent, RoomIndicator } from '../styles';
|
||||
|
||||
const RoomSelector = ({ selectedRoomId, onRoomChange }) => {
|
||||
const [rooms, setRooms] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchRooms = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.get('/api/rooms', { params: { pageSize: 1000 } });
|
||||
setRooms(response.data.rooms || []);
|
||||
if (!selectedRoomId && response.data.rooms?.length > 0) {
|
||||
onRoomChange(response.data.rooms[0].roomId);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取机房列表失败:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [selectedRoomId, onRoomChange]);
|
||||
const hasAutoSelected = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchRooms = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.get('/api/rooms', { params: { pageSize: 1000 } });
|
||||
const roomList = response.data.rooms || [];
|
||||
setRooms(roomList);
|
||||
|
||||
if (!hasAutoSelected.current && roomList.length > 0 && !selectedRoomId) {
|
||||
hasAutoSelected.current = true;
|
||||
onRoomChange(roomList[0].roomId);
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('获取机房列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchRooms();
|
||||
}, []);
|
||||
|
||||
@@ -36,15 +41,10 @@ const RoomSelector = ({ selectedRoomId, onRoomChange }) => {
|
||||
options={rooms.map(r => ({
|
||||
value: r.roomId,
|
||||
label: (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: '50%',
|
||||
background: '#1677ff',
|
||||
}} />
|
||||
<RoomOptionContent>
|
||||
<RoomIndicator />
|
||||
{r.name}
|
||||
</div>
|
||||
</RoomOptionContent>
|
||||
),
|
||||
}))}
|
||||
notFoundContent={loading ? <Spin size="small" /> : '暂无机房'}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { Input } from 'antd';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
|
||||
const SearchBar = ({ racks, onLocateRack }) => {
|
||||
const [keyword, setKeyword] = useState('');
|
||||
|
||||
const handleSearch = useCallback((value) => {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
setKeyword('');
|
||||
return;
|
||||
}
|
||||
|
||||
const matched = racks?.find(
|
||||
r => r.name?.toLowerCase().includes(trimmed.toLowerCase()) ||
|
||||
r.rackId?.toLowerCase().includes(trimmed.toLowerCase())
|
||||
);
|
||||
|
||||
if (matched) {
|
||||
onLocateRack(matched);
|
||||
}
|
||||
setKeyword(trimmed);
|
||||
}, [racks, onLocateRack]);
|
||||
|
||||
return (
|
||||
<Input.Search
|
||||
size="small"
|
||||
placeholder="搜索机柜..."
|
||||
prefix={<SearchOutlined />}
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
onSearch={handleSearch}
|
||||
style={{ width: 180 }}
|
||||
allowClear
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchBar;
|
||||
@@ -1,52 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Radio, Select, Space } from 'antd';
|
||||
import { AppstoreOutlined, FireOutlined, EditOutlined } from '@ant-design/icons';
|
||||
|
||||
const ViewModeSwitch = ({ viewMode, heatMapDimension, editMode, onViewModeChange, onEditModeChange }) => {
|
||||
return (
|
||||
<Space size={8}>
|
||||
<Radio.Group
|
||||
value={editMode ? 'edit' : viewMode}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
if (val === 'edit') {
|
||||
onEditModeChange(true);
|
||||
onViewModeChange('standard');
|
||||
} else {
|
||||
onEditModeChange(false);
|
||||
onViewModeChange(val);
|
||||
}
|
||||
}}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
size="small"
|
||||
>
|
||||
<Radio.Button value="standard">
|
||||
<AppstoreOutlined /> 标准
|
||||
</Radio.Button>
|
||||
<Radio.Button value="heatmap">
|
||||
<FireOutlined /> 热力图
|
||||
</Radio.Button>
|
||||
<Radio.Button value="edit">
|
||||
<EditOutlined /> 编辑
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
|
||||
{viewMode === 'heatmap' && !editMode && (
|
||||
<Select
|
||||
value={heatMapDimension}
|
||||
onChange={(val) => onViewModeChange('heatmap', val)}
|
||||
size="small"
|
||||
style={{ width: 120 }}
|
||||
options={[
|
||||
{ value: 'utilization', label: 'U位使用率' },
|
||||
{ value: 'power', label: '功率负载' },
|
||||
{ value: 'density', label: '设备密度' },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewModeSwitch;
|
||||
@@ -1,126 +1,47 @@
|
||||
import React from 'react';
|
||||
import { Button, Space, Tooltip } from 'antd';
|
||||
import { ZoomInOutlined, ZoomOutOutlined, OneToOneOutlined } from '@ant-design/icons';
|
||||
import { ZoomControlsWrapper, ZoomPercent } from '../styles';
|
||||
|
||||
const ZoomControls = ({ zoom, onZoomIn, onZoomOut, onZoomReset }) => {
|
||||
const percent = Math.round(zoom * 100);
|
||||
|
||||
return (
|
||||
<Space size={6} align="center" style={{ background: '#f8f9fa', padding: '6px 10px', borderRadius: 8 }}>
|
||||
<ZoomControlsWrapper>
|
||||
<Tooltip title="缩小" placement="bottom">
|
||||
<button
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<ZoomOutOutlined />}
|
||||
onClick={onZoomOut}
|
||||
disabled={zoom <= 0.3}
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: zoom <= 0.3 ? '#f5f5f5' : '#fff',
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: 6,
|
||||
cursor: zoom <= 0.3 ? 'not-allowed' : 'pointer',
|
||||
color: zoom <= 0.3 ? '#d9d9d9' : '#595959',
|
||||
transition: 'all 0.2s',
|
||||
fontSize: 14,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (zoom > 0.3) {
|
||||
e.target.style.background = '#e6f7ff';
|
||||
e.target.style.borderColor = '#91d5ff';
|
||||
e.target.style.color = '#1890ff';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.background = zoom <= 0.3 ? '#f5f5f5' : '#fff';
|
||||
e.target.style.borderColor = '#e8e8e8';
|
||||
e.target.style.color = zoom <= 0.3 ? '#d9d9d9' : '#595959';
|
||||
}}
|
||||
>
|
||||
<ZoomOutOutlined />
|
||||
</button>
|
||||
style={{ width: 28, height: 28, display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<div style={{
|
||||
minWidth: 52,
|
||||
textAlign: 'center',
|
||||
fontSize: 11,
|
||||
color: '#595959',
|
||||
userSelect: 'none',
|
||||
fontWeight: 500,
|
||||
padding: '0 4px',
|
||||
}}>
|
||||
{percent}%
|
||||
</div>
|
||||
<ZoomPercent>{percent}%</ZoomPercent>
|
||||
|
||||
<Tooltip title="放大" placement="bottom">
|
||||
<button
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<ZoomInOutlined />}
|
||||
onClick={onZoomIn}
|
||||
disabled={zoom >= 2}
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: zoom >= 2 ? '#f5f5f5' : '#fff',
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: 6,
|
||||
cursor: zoom >= 2 ? 'not-allowed' : 'pointer',
|
||||
color: zoom >= 2 ? '#d9d9d9' : '#595959',
|
||||
transition: 'all 0.2s',
|
||||
fontSize: 14,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (zoom < 2) {
|
||||
e.target.style.background = '#e6f7ff';
|
||||
e.target.style.borderColor = '#91d5ff';
|
||||
e.target.style.color = '#1890ff';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.background = zoom >= 2 ? '#f5f5f5' : '#fff';
|
||||
e.target.style.borderColor = '#e8e8e8';
|
||||
e.target.style.color = zoom >= 2 ? '#d9d9d9' : '#595959';
|
||||
}}
|
||||
>
|
||||
<ZoomInOutlined />
|
||||
</button>
|
||||
style={{ width: 28, height: 28, display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="重置缩放" placement="bottom">
|
||||
<button
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<OneToOneOutlined />}
|
||||
onClick={onZoomReset}
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#fff',
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: 6,
|
||||
cursor: 'pointer',
|
||||
color: '#595959',
|
||||
transition: 'all 0.2s',
|
||||
fontSize: 14,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.background = '#e6f7ff';
|
||||
e.target.style.borderColor = '#91d5ff';
|
||||
e.target.style.color = '#1890ff';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.background = '#fff';
|
||||
e.target.style.borderColor = '#e8e8e8';
|
||||
e.target.style.color = '#595959';
|
||||
}}
|
||||
>
|
||||
<OneToOneOutlined />
|
||||
</button>
|
||||
style={{ width: 28, height: 28, display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</ZoomControlsWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
export { default as FloorPlanToolbar } from './FloorPlanToolbar';
|
||||
export { default as RoomSelector } from './RoomSelector';
|
||||
export { default as ZoomControls } from './ZoomControls';
|
||||
export { default as ViewModeSwitch } from './ViewModeSwitch';
|
||||
export { default as SearchBar } from './SearchBar';
|
||||
|
||||
Reference in New Issue
Block a user