From 46d131cd045d12a54d4af807f85e839df0906918 Mon Sep 17 00:00:00 2001 From: zhang1106 <849185023@qq.com> Date: Mon, 29 Dec 2025 10:09:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=9C=BA=E6=9F=9C=E5=8F=AF=E8=A7=86?= =?UTF-8?q?=E5=8C=96):=20=E6=B7=BB=E5=8A=A0=E8=AE=BE=E5=A4=87=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E9=AB=98?= =?UTF-8?q?=E4=BA=AE=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增全局设备搜索功能,支持跨机柜搜索和定位 - 添加搜索结果显示面板,包含设备位置和机柜信息 - 实现设备高亮动画效果,增强视觉引导 - 优化UI交互,添加搜索状态提示和清除功能 --- backend/backgroundSettings.json | 4 +- frontend/src/pages/RackVisualization.jsx | 367 +++++++++++++++++++---- 2 files changed, 306 insertions(+), 65 deletions(-) diff --git a/backend/backgroundSettings.json b/backend/backgroundSettings.json index e5d2719..a3368f8 100644 --- a/backend/backgroundSettings.json +++ b/backend/backgroundSettings.json @@ -1,5 +1,5 @@ { - "type": "gradient", - "image": "/uploads/1766291127756_生成情侣图片.png", + "type": "image", + "image": null, "size": "contain" } \ No newline at end of file diff --git a/frontend/src/pages/RackVisualization.jsx b/frontend/src/pages/RackVisualization.jsx index 9bdc974..b21add4 100644 --- a/frontend/src/pages/RackVisualization.jsx +++ b/frontend/src/pages/RackVisualization.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; -import { Card, Select, Button, Space, message, Tooltip, Modal, Form, Switch, Checkbox } from 'antd'; +import { Card, Select, Button, Space, message, Tooltip, Modal, Form, Switch, Checkbox, Input, Badge, Typography } from 'antd'; import { ReloadOutlined, ZoomInOutlined, @@ -12,11 +12,15 @@ import { LaptopOutlined, MobileOutlined, PrinterOutlined, - SettingOutlined + SettingOutlined, + SearchOutlined, + ClearOutlined, + EnvironmentOutlined } from '@ant-design/icons'; import axios from 'axios'; const { Option } = Select; +const { Text } = Typography; // 工具函数提取到组件外部,避免每次渲染重复创建 const getDeviceIcon = (deviceType) => { @@ -177,6 +181,27 @@ const initAnimationStyles = () => { } } + @keyframes highlightPulse { + 0%, 100% { + box-shadow: + 0 0 20px rgba(56, 189, 248, 0.6), + 0 4px 12px rgba(56, 189, 248, 0.4), + 0 1px 2px rgba(0,0,0,0.3), + inset 0 1px 0 rgba(255,255,255,0.2); + } + 50% { + box-shadow: + 0 0 35px rgba(56, 189, 248, 0.9), + 0 6px 20px rgba(56, 189, 248, 0.6), + 0 1px 2px rgba(0,0,0,0.3), + inset 0 1px 0 rgba(255,255,255,0.3); + } + } + + .device-highlighted { + animation: highlightPulse 1.5s ease-in-out infinite; + } + .metal-texture { background-image: linear-gradient(90deg, @@ -312,6 +337,13 @@ function RackVisualization() { const [loadingTooltipFields, setLoadingTooltipFields] = useState(false); // 加载字段配置状态 const [savingTooltipConfig, setSavingTooltipConfig] = useState(false); // 保存配置状态 + // 设备搜索功能 + const [searchKeyword, setSearchKeyword] = useState(''); // 搜索关键词 + const [searchResults, setSearchResults] = useState([]); // 搜索结果 + const [highlightedDevice, setHighlightedDevice] = useState(null); // 高亮的设备 + const [searchMatchCount, setSearchMatchCount] = useState(0); // 匹配数量 + const [searching, setSearching] = useState(false); // 搜索中状态 + // 设备详情tooltip字段配置 const [tooltipFields, setTooltipFields] = useState({}); @@ -374,6 +406,11 @@ function RackVisualization() { } }, [defaultTooltipFields]); + // 初始化获取保存的字段配置 + useEffect(() => { + fetchTooltipDeviceFields(); + }, [fetchTooltipDeviceFields]); + // 保存tooltip字段配置 const saveTooltipConfig = useCallback(async () => { try { @@ -400,55 +437,6 @@ function RackVisualization() { } }, [tooltipFields, fetchTooltipDeviceFields]); - // 获取所有机柜 - 使用 useCallback 避免重复创建 - const fetchRacks = useCallback(async () => { - try { - setLoading(true); - setError(null); - // 为机柜可视化页面传递较大的pageSize以获取所有机柜 - const response = await axios.get('/api/racks', { params: { pageSize: 1000 } }); - - // 验证数据结构并过滤有效机柜 - // 机柜API返回的是包含total和racks数组的对象 - const racksArray = response.data.racks || []; - const validRacks = racksArray.filter(rack => - rack && - typeof rack === 'object' && - rack.rackId && - typeof rack.height === 'number' - ); - - setRacks(validRacks); - if (validRacks.length > 0) { - // 自动选择第一个机柜 - const firstRack = validRacks[0]; - setSelectedRack(firstRack); - - // 设置对应的机房 - if (firstRack?.Room) { - const roomKey = firstRack.Room.roomId || firstRack.Room.id || firstRack.Room.name; - setSelectedRoom(roomKey); - } - - fetchDevices(firstRack.rackId); - } else { - setSelectedRack(null); - setSelectedRoom(null); - setDevices([]); - message.warning('未找到有效的机柜数据'); - } - } catch (error) { - message.error('获取机柜列表失败'); - console.error('获取机柜列表失败:', error); - setError(error); - setRacks([]); - setSelectedRack(null); - setDevices([]); - } finally { - setLoading(false); - } - }, []); - // 获取机柜内的设备 - 使用 useCallback 避免重复创建 const fetchDevices = useCallback(async (rackId) => { try { @@ -594,6 +582,144 @@ function RackVisualization() { } }, []); + // 搜索设备 - 全局搜索,支持跨机柜 + const handleSearch = useCallback(async (keyword) => { + setSearchKeyword(keyword); + + if (!keyword || !keyword.trim()) { + setSearchResults([]); + setHighlightedDevice(null); + setSearchMatchCount(0); + return; + } + + const searchLower = keyword.toLowerCase().trim(); + setSearching(true); + + try { + // 调用全局搜索API,搜索所有机柜的设备 + const response = await axios.get('/api/devices', { + params: { keyword: searchLower, pageSize: 100 } + }); + + const devicesData = response.data.devices || []; + + // 构建搜索结果,包含机柜信息 + const results = devicesData.map(device => ({ + deviceId: device.deviceId || device.id, + name: device.name, + type: device.type, + rackId: device.rackId, + rackName: device.Rack?.name || '未知机柜', + roomName: device.Rack?.Room?.name || '未知机房', + position: device.position + })); + + setSearchResults(results); + setSearchMatchCount(results.length); + + // 如果有搜索结果,自动定位到第一个设备所在的机柜 + if (results.length > 0) { + const firstResult = results[0]; + setHighlightedDevice(firstResult.deviceId); + + // 如果设备不在当前机柜,自动切换机柜 + if (selectedRack && firstResult.rackId !== selectedRack.rackId) { + const targetRack = racks.find(r => r.rackId === firstResult.rackId); + if (targetRack) { + // 找到设备所在的机房 + if (targetRack?.Room) { + const roomKey = targetRack.Room.roomId || targetRack.Room.id || targetRack.Room.name; + if (roomKey !== selectedRoom) { + setSelectedRoom(roomKey); + } + } + + // 切换到设备所在的机柜 + setSelectedRack(targetRack); + fetchDevices(targetRack.rackId); + message.info(`已跳转到机柜: ${firstResult.rackName}`); + } + } else { + message.info(`已定位到设备: ${firstResult.name}`); + } + } else { + setHighlightedDevice(null); + message.warning('未找到匹配的设备'); + } + } catch (error) { + console.error('全局搜索失败:', error); + message.error('搜索失败,请重试'); + setSearchResults([]); + setSearchMatchCount(0); + } finally { + setSearching(false); + } + }, [selectedRack, selectedRoom, racks, fetchDevices]); + + // 清除搜索 + const clearSearch = useCallback(() => { + setSearchKeyword(''); + setSearchResults([]); + setHighlightedDevice(null); + setSearchMatchCount(0); + }, []); + + // 跳转到指定设备 + const jumpToDevice = useCallback((deviceId) => { + setHighlightedDevice(deviceId); + message.info(`已定位到设备: ${deviceId}`); + }, []); + + // 获取所有机柜 - 使用 useCallback 避免重复创建 + const fetchRacks = useCallback(async () => { + try { + setLoading(true); + setError(null); + // 为机柜可视化页面传递较大的pageSize以获取所有机柜 + const response = await axios.get('/api/racks', { params: { pageSize: 1000 } }); + + // 验证数据结构并过滤有效机柜 + // 机柜API返回的是包含total和racks数组的对象 + const racksArray = response.data.racks || []; + const validRacks = racksArray.filter(rack => + rack && + typeof rack === 'object' && + rack.rackId && + typeof rack.height === 'number' + ); + + setRacks(validRacks); + if (validRacks.length > 0) { + // 自动选择第一个机柜 + const firstRack = validRacks[0]; + setSelectedRack(firstRack); + + // 设置对应的机房 + if (firstRack?.Room) { + const roomKey = firstRack.Room.roomId || firstRack.Room.id || firstRack.Room.name; + setSelectedRoom(roomKey); + } + + fetchDevices(firstRack.rackId); + } else { + setSelectedRack(null); + setSelectedRoom(null); + setDevices([]); + message.warning('未找到有效的机柜数据'); + } + } catch (error) { + message.error('获取机柜列表失败'); + console.error('获取机柜列表失败:', error); + setError(error); + setRacks([]); + setSelectedRack(null); + setDevices([]); + } finally { + setLoading(false); + } + }, []); + useEffect(() => { fetchRacks(); }, [fetchRacks]); @@ -914,6 +1040,27 @@ function RackVisualization() { title="机柜可视化" extra={ + handleSearch(e.target.value)} + onPressEnter={(e) => handleSearch(e.target.value)} + style={{ width: 220 }} + allowClear + prefix={} + suffix={ + searchMatchCount > 0 ? ( + + ) : null + } + /> + {searchKeyword && ( +