refactor(floorplan): 优化机房平面图交互与代码结构

- 重构设备悬停提示逻辑,使用画布坐标替代DOM坐标
- 提取缩放控制逻辑到独立回调函数
- 移除未使用的组件和引用
- 改进全屏切换实现方式
- 添加平面图导出功能
This commit is contained in:
zhang1106
2026-05-08 13:07:27 +08:00
parent 2826a00192
commit a5505207bc
2 changed files with 80 additions and 60 deletions
@@ -16,7 +16,7 @@ const RoomSelector = ({ selectedRoomId, onRoomChange }) => {
const response = await axios.get('/api/rooms', { params: { pageSize: 1000 } }); const response = await axios.get('/api/rooms', { params: { pageSize: 1000 } });
const roomList = response.data.rooms || []; const roomList = response.data.rooms || [];
setRooms(roomList); setRooms(roomList);
if (!hasAutoSelected.current && roomList.length > 0 && !selectedRoomId) { if (!hasAutoSelected.current && roomList.length > 0 && !selectedRoomId) {
hasAutoSelected.current = true; hasAutoSelected.current = true;
onRoomChange(roomList[0].roomId); onRoomChange(roomList[0].roomId);
+79 -59
View File
@@ -1,6 +1,5 @@
import React, { useRef, useCallback, useState, useEffect } from 'react'; import React, { useRef, useCallback, useState, useEffect } from 'react';
import { Spin, Empty, message, Button } from 'antd'; import { Spin, Empty, message } from 'antd';
import { HomeOutlined, ReloadOutlined } from '@ant-design/icons';
import useFloorPlanContext from '../hooks/floorplan/useFloorPlanContext'; import useFloorPlanContext from '../hooks/floorplan/useFloorPlanContext';
import useFloorPlanData from '../hooks/floorplan/useFloorPlanData'; import useFloorPlanData from '../hooks/floorplan/useFloorPlanData';
import { FloorPlanCanvas, FloorPlanToolbar, RackDetailPanel } from '../components/floorplan'; import { FloorPlanCanvas, FloorPlanToolbar, RackDetailPanel } from '../components/floorplan';
@@ -16,8 +15,6 @@ import {
CanvasContainer, CanvasContainer,
LoadingOverlay, LoadingOverlay,
EmptyStateContainer, EmptyStateContainer,
EmptyStateTitle,
EmptyStateSubtitle,
DeviceTooltipContainer, DeviceTooltipContainer,
DeviceTooltipHeader, DeviceTooltipHeader,
DeviceTypeIndicator, DeviceTypeIndicator,
@@ -80,13 +77,16 @@ const RoomFloorPlanContent = () => {
const { layoutData, loading, error, refetch } = useFloorPlanData(selectedRoomId); const { layoutData, loading, error, refetch } = useFloorPlanData(selectedRoomId);
const canvasRef = useRef(null); const canvasRef = useRef(null);
const containerRef = useRef(null);
const [currentZoom, setCurrentZoom] = useState(1); const [currentZoom, setCurrentZoom] = useState(1);
const [hoveredDevice, setHoveredDevice] = useState(null); const [hoveredDevice, setHoveredDevice] = useState(null);
const [hoveredDeviceRack, setHoveredDeviceRack] = useState(null); const [hoveredDeviceRack, setHoveredDeviceRack] = useState(null);
const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 }); const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
const handleViewChange = useCallback(({ zoom }) => {
setCurrentZoom(zoom);
}, []);
useEffect(() => { useEffect(() => {
if (error) { if (error) {
message.error(error); message.error(error);
@@ -110,10 +110,10 @@ const RoomFloorPlanContent = () => {
}, []); }, []);
const handleToggleFullscreen = useCallback(() => { const handleToggleFullscreen = useCallback(() => {
if (!containerRef.current) return; const elem = document.querySelector('[data-floorplan-container]');
if (!elem) return;
if (!document.fullscreenElement) { if (!document.fullscreenElement) {
const elem = containerRef.current;
if (elem.requestFullscreen) { if (elem.requestFullscreen) {
elem.requestFullscreen(); elem.requestFullscreen();
} else if (elem.webkitRequestFullscreen) { } else if (elem.webkitRequestFullscreen) {
@@ -132,15 +132,12 @@ const RoomFloorPlanContent = () => {
} }
}, []); }, []);
const handleDeviceHover = useCallback((device, rack, event) => { const handleDeviceHover = useCallback((device, rack, canvasX, canvasY) => {
if (device) { if (device) {
const rect = containerRef.current?.getBoundingClientRect(); setTooltipPosition({
if (rect) { x: (canvasX || 0) + 15,
setTooltipPosition({ y: (canvasY || 0) + 15,
x: event.clientX - rect.left + 15, });
y: event.clientY - rect.top + 15,
});
}
setHoveredDevice(device); setHoveredDevice(device);
setHoveredDeviceRack(rack); setHoveredDeviceRack(rack);
} else { } else {
@@ -155,63 +152,86 @@ const RoomFloorPlanContent = () => {
} }
}, [showDetail]); }, [showDetail]);
if (loading) { const roomName = layoutData?.room?.name || '';
return (
<PageContainer>
<LoadingOverlay>
<Spin size="large" />
<span>加载中...</span>
</LoadingOverlay>
</PageContainer>
);
}
if (!layoutData || layoutData.racks.length === 0) { const handleZoomIn = useCallback(() => {
return ( canvasRef.current?.zoomIn();
<PageContainer> }, []);
<EmptyStateContainer>
<Empty const handleZoomOut = useCallback(() => {
description={selectedRoomId ? '该机房暂无机柜数据' : '请先选择机房'} canvasRef.current?.zoomOut();
/> }, []);
</EmptyStateContainer>
</PageContainer> const handleZoomReset = useCallback(() => {
); canvasRef.current?.zoomReset();
} }, []);
const handleExport = useCallback(() => {
const dataUrl = canvasRef.current?.exportImage(roomName);
if (!dataUrl) {
message.warning('暂无数据可导出');
return;
}
const link = document.createElement('a');
link.download = `${roomName || '机房'}_平面图.png`;
link.href = dataUrl;
link.click();
}, [roomName]);
return ( return (
<PageContainer> <PageContainer>
<FloorPlanToolbar <FloorPlanToolbar
roomName={layoutData.roomName} roomName={roomName}
roomId={selectedRoomId} roomId={selectedRoomId}
onRoomChange={setSelectedRoom} onRoomChange={setSelectedRoom}
zoom={currentZoom} zoom={currentZoom}
onZoomChange={setCurrentZoom} onZoomIn={handleZoomIn}
onZoomOut={handleZoomOut}
onZoomReset={handleZoomReset}
onFullscreen={handleToggleFullscreen} onFullscreen={handleToggleFullscreen}
isFullscreen={isFullscreen} isFullscreen={isFullscreen}
onRefresh={refetch} onRefresh={refetch}
onExport={handleExport}
/> />
<ContentWrapper ref={containerRef}> {loading ? (
<CanvasContainer> <ContentWrapper>
<FloorPlanCanvas <LoadingOverlay>
racks={layoutData.racks} <Spin size="large" />
rooms={layoutData.rooms} <span>加载中...</span>
selectedRoomId={selectedRoomId} </LoadingOverlay>
zoom={currentZoom} </ContentWrapper>
onRackClick={handleRackClick} ) : !layoutData || layoutData.racks.length === 0 ? (
onDeviceHover={handleDeviceHover} <ContentWrapper>
/> <EmptyStateContainer>
<Empty
{hoveredDevice && ( description={selectedRoomId ? '该机房暂无机柜数据' : '请先选择机房'}
<DeviceTooltip
device={hoveredDevice}
rack={hoveredDeviceRack}
x={tooltipPosition.x}
y={tooltipPosition.y}
/> />
)} </EmptyStateContainer>
</CanvasContainer> </ContentWrapper>
</ContentWrapper> ) : (
<ContentWrapper data-floorplan-container>
<CanvasContainer>
<FloorPlanCanvas
ref={canvasRef}
room={layoutData.room}
racks={layoutData.racks}
onRackClick={handleRackClick}
onDeviceHover={handleDeviceHover}
onViewChange={handleViewChange}
/>
{hoveredDevice && (
<DeviceTooltip
device={hoveredDevice}
rack={hoveredDeviceRack}
x={tooltipPosition.x}
y={tooltipPosition.y}
/>
)}
</CanvasContainer>
</ContentWrapper>
)}
{detailVisible && detailRack && ( {detailVisible && detailRack && (
<RackDetailPanel <RackDetailPanel