feat: 添加响应式3D机柜可视化组件和钩子
重构3D机柜可视化页面,添加以下功能: 1. 新增 useResponsiveLayout 钩子处理响应式布局 2. 新增 useSortedRacks 钩子实现机柜排序和筛选 3. 添加 CascadingRackPanel 级联选择面板组件 4. 实现 RackSelectorHeader 响应式头部导航组件 5. 优化页面结构和交互逻辑
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user