feat: 添加响应式3D机柜可视化组件和钩子

重构3D机柜可视化页面,添加以下功能:
1. 新增 useResponsiveLayout 钩子处理响应式布局
2. 新增 useSortedRacks 钩子实现机柜排序和筛选
3. 添加 CascadingRackPanel 级联选择面板组件
4. 实现 RackSelectorHeader 响应式头部导航组件
5. 优化页面结构和交互逻辑
This commit is contained in:
zhang1106
2026-03-25 12:14:00 +08:00
parent 6ebcb3f870
commit 0b7732e427
5 changed files with 1186 additions and 335 deletions
+103
View File
@@ -0,0 +1,103 @@
import { useState, useEffect, useCallback } from 'react';
export const BREAKPOINTS = {
xs: 0,
sm: 768,
md: 992,
lg: 1200,
xl: 1440,
};
export const SCREEN_SIZES = {
xs: 'xs',
sm: 'sm',
md: 'md',
lg: 'lg',
xl: 'xl',
};
const getScreenSize = (width) => {
if (width >= BREAKPOINTS.xl) return SCREEN_SIZES.xl;
if (width >= BREAKPOINTS.lg) return SCREEN_SIZES.lg;
if (width >= BREAKPOINTS.md) return SCREEN_SIZES.md;
if (width >= BREAKPOINTS.sm) return SCREEN_SIZES.sm;
return SCREEN_SIZES.xs;
};
const getScreenSizeConfig = (screenSize) => {
const configs = {
xs: {
showFullButtonLabels: false,
buttonIconOnly: true,
showDeviceSlide: false,
panelFullWidth: true,
collapseActions: true,
},
sm: {
showFullButtonLabels: false,
buttonIconOnly: true,
showDeviceSlide: false,
panelFullWidth: true,
collapseActions: true,
},
md: {
showFullButtonLabels: false,
buttonIconOnly: true,
showDeviceSlide: true,
panelFullWidth: false,
collapseActions: true,
},
lg: {
showFullButtonLabels: true,
buttonIconOnly: false,
showDeviceSlide: true,
panelFullWidth: false,
collapseActions: false,
},
xl: {
showFullButtonLabels: true,
buttonIconOnly: false,
showDeviceSlide: true,
panelFullWidth: false,
collapseActions: false,
},
};
return configs[screenSize] || configs.lg;
};
export const useResponsiveLayout = () => {
const [screenSize, setScreenSize] = useState(() => getScreenSize(window.innerWidth));
const [config, setConfig] = useState(() => getScreenSizeConfig(screenSize));
const handleResize = useCallback(() => {
const width = window.innerWidth;
const newScreenSize = getScreenSize(width);
if (newScreenSize !== screenSize) {
setScreenSize(newScreenSize);
setConfig(getScreenSizeConfig(newScreenSize));
}
}, [screenSize]);
useEffect(() => {
window.addEventListener('resize', handleResize);
handleResize();
return () => window.removeEventListener('resize', handleResize);
}, [handleResize]);
const isMobile = screenSize === SCREEN_SIZES.xs || screenSize === SCREEN_SIZES.sm;
const isTablet = screenSize === SCREEN_SIZES.md;
const isDesktop = screenSize === SCREEN_SIZES.lg || screenSize === SCREEN_SIZES.xl;
return {
screenSize,
config,
breakpoints: BREAKPOINTS,
isMobile,
isTablet,
isDesktop,
width: window.innerWidth,
height: window.innerHeight,
};
};
export default useResponsiveLayout;
+126
View File
@@ -0,0 +1,126 @@
import { useMemo } from 'react';
const extractNumberFromString = (str) => {
if (!str) return 0;
const match = str.match(/\d+/);
return match ? parseInt(match[0], 10) : 0;
};
const naturalSort = (a, b) => {
const numA = extractNumberFromString(a);
const numB = extractNumberFromString(b);
if (numA !== numB) return numA - numB;
return String(a).localeCompare(String(b), 'zh-CN');
};
const sortRooms = (rooms) => {
return [...rooms].sort((a, b) => {
const sortOrderA = a.sortOrder ?? a.sort_order ?? 0;
const sortOrderB = b.sortOrder ?? b.sort_order ?? 0;
if (sortOrderA !== sortOrderB) {
return sortOrderA - sortOrderB;
}
return (a.name || '').localeCompare(b.name || '', 'zh-CN');
});
};
const sortRacksInRoom = (racks) => {
return [...racks].sort((a, b) => {
const sortOrderA = a.sortOrder ?? a.sort_order ?? 0;
const sortOrderB = b.sortOrder ?? b.sort_order ?? 0;
if (sortOrderA !== sortOrderB) {
return sortOrderA - sortOrderB;
}
const numA = extractNumberFromString(a.name);
const numB = extractNumberFromString(b.name);
if (numA !== numB) return numA - numB;
return (a.name || '').localeCompare(b.name || '', 'zh-CN');
});
};
export const useSortedRacks = (racks) => {
return useMemo(() => {
if (!racks || !Array.isArray(racks) || racks.length === 0) {
return [];
}
const roomMap = new Map();
racks.forEach((rack) => {
if (!rack || !rack.Room) return;
const roomKey = rack.Room.roomId || rack.Room.id || rack.Room.name;
if (!roomKey) return;
if (!roomMap.has(roomKey)) {
roomMap.set(roomKey, {
...rack.Room,
key: roomKey,
roomId: roomKey,
racks: [],
});
}
roomMap.get(roomKey).racks.push(rack);
});
const sortedRooms = sortRooms(Array.from(roomMap.values()));
sortedRooms.forEach((room) => {
room.racks = sortRacksInRoom(room.racks);
room.rackCount = room.racks.length;
room.totalDevices = room.racks.reduce((sum, r) => sum + (r._count?.Devices || r.deviceCount || 0), 0);
});
return sortedRooms;
}, [racks]);
};
export const filterRoomsBySearch = (rooms, searchText) => {
if (!searchText || searchText.trim() === '') {
return rooms;
}
const lowerSearch = searchText.toLowerCase().trim();
return rooms
.map((room) => {
const roomNameMatch = room.name?.toLowerCase().includes(lowerSearch);
const roomIdMatch = room.roomId?.toLowerCase().includes(lowerSearch);
const filteredRacks = room.racks.filter((rack) => {
const rackNameMatch = rack.name?.toLowerCase().includes(lowerSearch);
const rackIdMatch = rack.rackId?.toLowerCase().includes(lowerSearch);
return roomNameMatch || roomIdMatch || rackNameMatch || rackIdMatch;
});
if (roomNameMatch || roomIdMatch) {
return { ...room, racks: room.racks };
}
if (filteredRacks.length > 0) {
return { ...room, racks: filteredRacks };
}
return null;
})
.filter((room) => room !== null);
};
export const getRackStats = (rack, devices = []) => {
const usedU = devices.reduce((sum, d) => sum + (d.height || d.u_height || 1), 0);
const totalPower = devices.reduce((sum, d) => sum + (parseFloat(d.powerConsumption) || 0), 0);
const totalHeight = rack.height || 45;
const deviceCount = devices.length;
const usagePercent = totalHeight > 0 ? Math.round((usedU / totalHeight) * 100) : 0;
return {
usedU,
availableU: totalHeight - usedU,
usagePercent,
totalPower,
totalHeight,
deviceCount,
};
};
export default useSortedRacks;