feat: 添加请求参数验证中间件和设计令牌Hook

feat(validation): 为机房、机柜和设备路由添加Joi验证中间件

feat(hooks): 创建useDesignTokens Hook集中管理主题配置

feat(models): 为耗材模型添加乐观锁version字段和updatedAt索引

feat(3d): 增强3D场景和设备模型视觉效果与交互

refactor: 移除数据备份相关功能并优化代码结构

fix: 修复前端安全日志和密码加密工具

chore: 更新依赖并添加axios和joi库

docs: 更新注释和文档说明

style: 改进代码格式和命名一致性
This commit is contained in:
zhang1106
2026-01-29 16:53:29 +08:00
parent 6d61d6e54c
commit 9e6a3da888
23 changed files with 1518 additions and 1858 deletions
+92 -21
View File
@@ -179,7 +179,8 @@ const InstancedStatusLights = ({ count, positions, colors: statusColors, zOffset
useEffect(() => {
return () => {
if (meshRef.current) {
meshRef.current.dispose();
meshRef.current.geometry?.dispose();
meshRef.current.material?.dispose();
}
};
}, []);
@@ -219,8 +220,14 @@ const InstancedDriveBays = ({ count, positions, color, hasDetail = true }) => {
// 资源清理
useEffect(() => {
return () => {
if (meshRef.current) meshRef.current.dispose();
if (detailRef.current) detailRef.current.dispose();
if (meshRef.current) {
meshRef.current.geometry?.dispose();
meshRef.current.material?.dispose();
}
if (detailRef.current) {
detailRef.current.geometry?.dispose();
detailRef.current.material?.dispose();
}
};
}, []);
@@ -265,8 +272,14 @@ const InstancedStorageBays = ({ count, positions, color }) => {
// 资源清理
useEffect(() => {
return () => {
if (meshRef.current) meshRef.current.dispose();
if (detailRef.current) detailRef.current.dispose();
if (meshRef.current) {
meshRef.current.geometry?.dispose();
meshRef.current.material?.dispose();
}
if (detailRef.current) {
detailRef.current.geometry?.dispose();
detailRef.current.material?.dispose();
}
};
}, []);
@@ -335,10 +348,22 @@ const InstancedRJ45Ports = ({ count, positions, statuses, frontZ }) => {
// 资源清理
useEffect(() => {
return () => {
if (meshRef.current) meshRef.current.dispose();
if (innerRef.current) innerRef.current.dispose();
if (tabRef.current) tabRef.current.dispose();
if (ledRef.current) ledRef.current.dispose();
if (meshRef.current) {
meshRef.current.geometry?.dispose();
meshRef.current.material?.dispose();
}
if (innerRef.current) {
innerRef.current.geometry?.dispose();
innerRef.current.material?.dispose();
}
if (tabRef.current) {
tabRef.current.geometry?.dispose();
tabRef.current.material?.dispose();
}
if (ledRef.current) {
ledRef.current.geometry?.dispose();
ledRef.current.material?.dispose();
}
};
}, []);
@@ -421,10 +446,22 @@ const InstancedSFPports = ({ count, positions, statuses, frontZ }) => {
// 资源清理
useEffect(() => {
return () => {
if (meshRef.current) meshRef.current.dispose();
if (innerRef.current) innerRef.current.dispose();
if (connectorRef.current) connectorRef.current.dispose();
if (ledRef.current) ledRef.current.dispose();
if (meshRef.current) {
meshRef.current.geometry?.dispose();
meshRef.current.material?.dispose();
}
if (innerRef.current) {
innerRef.current.geometry?.dispose();
innerRef.current.material?.dispose();
}
if (connectorRef.current) {
connectorRef.current.geometry?.dispose();
connectorRef.current.material?.dispose();
}
if (ledRef.current) {
ledRef.current.geometry?.dispose();
ledRef.current.material?.dispose();
}
};
}, []);
@@ -443,10 +480,16 @@ const FirewallFace = ({ device, height, frontZ, isSelected }) => {
return (
<group>
{/* 防火墙左侧红色标识条 */}
<mesh position={[-halfWidth + 0.02, 0, frontZ + 0.006]}>
<boxGeometry args={[0.01, height, 0.003]} />
<boxGeometry args={[0.015, height - 0.008, 0.003]} />
<meshStandardMaterial color="#ef4444" emissive="#ef4444" emissiveIntensity={0.3} />
</mesh>
{/* 防火墙主面板边框 */}
<mesh position={[-halfWidth + 0.035, 0, frontZ + 0.005]}>
<boxGeometry args={[0.005, height - 0.004, 0.002]} />
<meshStandardMaterial color="#dc2626" />
</mesh>
<mesh position={[0, 0, frontZ + 0.0055]}>
<boxGeometry args={[0.46, height - 0.004, 0.002]} />
@@ -631,10 +674,16 @@ const DeviceModel = ({ device, rackHeight, isSelected, onClick, onHover, uHeight
return (
<group>
{/* 服务器主面板 - 深蓝色 */}
<mesh position={[0, 0, frontZ + 0.0055]}>
<boxGeometry args={[0.44, height - 0.004, 0.002]} />
<meshStandardMaterial color="#1e293b" roughness={0.7} metalness={0.5} />
</mesh>
{/* 服务器左侧标识条 - 亮蓝色 */}
<mesh position={[-0.21, 0, frontZ + 0.006]}>
<boxGeometry args={[0.015, height - 0.008, 0.003]} />
<meshStandardMaterial color="#3b82f6" emissive="#3b82f6" emissiveIntensity={0.2} />
</mesh>
<group position={[-0.18, 0, frontZ + 0.006]}>
<mesh position={[-0.02, 0, 0]}>
@@ -710,11 +759,17 @@ const DeviceModel = ({ device, rackHeight, isSelected, onClick, onHover, uHeight
return (
<group>
{/* 存储设备主面板 - 深紫色 */}
<mesh position={[0, 0, frontZ + 0.0055]}>
<boxGeometry args={[0.44, height - 0.004, 0.002]} />
<meshStandardMaterial color="#0f172a" roughness={0.8} />
</mesh>
{/* 存储设备左侧标识条 - 紫色 */}
<mesh position={[-0.21, 0, frontZ + 0.006]}>
<boxGeometry args={[0.015, height - 0.008, 0.003]} />
<meshStandardMaterial color="#8b5cf6" emissive="#8b5cf6" emissiveIntensity={0.2} />
</mesh>
<group position={[0, 0, frontZ + 0.008]}>
<InstancedStorageBays
count={bayPositions.length}
@@ -769,9 +824,20 @@ const DeviceModel = ({ device, rackHeight, isSelected, onClick, onHover, uHeight
return (
<group>
{/* 交换机主面板 - 深绿色背景突出显示 */}
<mesh position={[0, 0, frontZ + 0.0055]}>
<boxGeometry args={[0.44, height - 0.004, 0.002]} />
<meshStandardMaterial color="#334155" roughness={0.6} metalness={0.4} />
<meshStandardMaterial color="#064e3b" roughness={0.5} metalness={0.5} />
</mesh>
{/* 交换机左侧标识条 - 亮绿色 */}
<mesh position={[-0.21, 0, frontZ + 0.006]}>
<boxGeometry args={[0.015, height - 0.008, 0.003]} />
<meshStandardMaterial color="#10b981" emissive="#10b981" emissiveIntensity={0.2} />
</mesh>
{/* 交换机类型标识 */}
<mesh position={[-0.19, height/2 - 0.015, frontZ + 0.007]}>
<boxGeometry args={[0.025, 0.012, 0.002]} />
<meshStandardMaterial color="#065f46" />
</mesh>
<group position={[-0.19, 0, frontZ + 0.006]}>
@@ -969,13 +1035,18 @@ const DeviceModel = ({ device, rackHeight, isSelected, onClick, onHover, uHeight
return (
<group>
{/* 默认面板纹理 */}
<mesh position={[0, 0, frontZ + 0.005]}>
<boxGeometry args={[0.7, height - gap - 0.01, 0.002]} />
<meshStandardMaterial color="#1e293b" roughness={0.6} />
<mesh position={[0, 0, frontZ + 0.0055]}>
<boxGeometry args={[0.44, height - 0.004, 0.002]} />
<meshStandardMaterial color="#374151" roughness={0.6} metalness={0.4} />
</mesh>
{/* 左侧灰色标识条 */}
<mesh position={[-0.21, 0, frontZ + 0.006]}>
<boxGeometry args={[0.015, height - 0.008, 0.003]} />
<meshStandardMaterial color="#6b7280" emissive="#6b7280" emissiveIntensity={0.1} />
</mesh>
{/* 装饰线 */}
<mesh position={[0, 0, frontZ + 0.006]}>
<boxGeometry args={[0.6, 0.005, 0.001]} />
<mesh position={[0, 0, frontZ + 0.007]}>
<boxGeometry args={[0.4, 0.003, 0.001]} />
<meshStandardMaterial color={deviceColor} />
</mesh>
</group>
+88 -24
View File
@@ -1,5 +1,5 @@
import React, { Suspense, useMemo, useRef, useEffect } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
import React, { Suspense, useMemo, useRef, useEffect, forwardRef, useImperativeHandle } from 'react';
import { Canvas, useFrame, useThree } from '@react-three/fiber';
import { OrbitControls, PerspectiveCamera, Environment } from '@react-three/drei';
const envMapUrl = '/assets/3d/env.hdr';
import RackModel from './RackModel';
@@ -10,16 +10,50 @@ import * as THREE from 'three';
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
// 检测是否为小屏幕
const isSmallScreen = window.innerWidth < 768;
// 移动端或小屏幕使用 dpr=1,桌面端使用 dpr=[1, 1.5]
const deviceDpr = isMobile || isSmallScreen ? 1 : [1, 1.5];
// 移动端或小屏幕使用 dpr=1,桌面端使用 dpr=[1, 2] 提升清晰度
const deviceDpr = isMobile || isSmallScreen ? 1 : [1, 2];
// 创建全局 ref 用于外部访问 controls
const controlsRefGlobal = { current: null };
// 内部组件用于处理 OrbitControls
const Controls = ({ rack }) => {
const Controls = ({ rack, onControlsReady }) => {
const controlsRef = useRef();
const { camera } = useThree();
// 机柜中心点(中轴线)
const targetY = (rack?.height || 45) * 0.04445 / 2 + 0.5;
const rackHeight = rack?.height || 45;
const targetY = rackHeight * 0.04445 / 2 + 0.5;
const fixedTarget = useMemo(() => new THREE.Vector3(0, targetY, 0), [targetY]);
// 根据机柜高度计算合适的相机距离限制
const minDistance = useMemo(() => Math.max(1.5, rackHeight * 0.04445 * 0.3), [rackHeight]);
const maxDistance = useMemo(() => Math.max(8, rackHeight * 0.04445 * 1.5), [rackHeight]);
// 保存相机初始位置用于重置
const initialCameraPosition = useMemo(() => {
const rackHeightMeters = rackHeight * 0.04445;
const baseHeight = 2;
const heightFactor = rackHeightMeters * 0.6;
const distance = Math.max(3, rackHeightMeters * 1.2);
return new THREE.Vector3(distance * 0.7, baseHeight + heightFactor * 0.3, distance);
}, [rackHeight]);
useEffect(() => {
if (controlsRef.current) {
controlsRefGlobal.current = controlsRef.current;
if (onControlsReady) {
onControlsReady({
reset: () => {
// 重置相机位置
camera.position.copy(initialCameraPosition);
controlsRef.current.target.copy(fixedTarget);
controlsRef.current.update();
}
});
}
}
}, [controlsRef.current, camera, initialCameraPosition, fixedTarget, onControlsReady]);
useFrame(() => {
if (controlsRef.current) {
// 强制保持 target 在机柜中轴线
@@ -32,17 +66,19 @@ const Controls = ({ rack }) => {
<OrbitControls
ref={controlsRef}
makeDefault
minPolarAngle={0}
maxPolarAngle={Math.PI / 1.75}
minPolarAngle={0.1}
maxPolarAngle={Math.PI / 1.5}
minAzimuthAngle={-Infinity}
maxAzimuthAngle={Infinity}
minDistance={minDistance}
maxDistance={maxDistance}
enablePan={true}
enableZoom={true}
enableRotate={true}
mouseButtons={{
LEFT: 0, // 左键旋转
MIDDLE: 0, // 中键禁用(避免平移改变旋转中心)
RIGHT: 0 // 右键禁用
MIDDLE: 1, // 中键平移
RIGHT: 2 // 右键平移
}}
touches={{
ONE: 1,
@@ -52,7 +88,7 @@ const Controls = ({ rack }) => {
);
};
const Scene = ({ rack, tooltipFields, onDeviceClick, onDeviceHover, onDeviceLeave }) => {
const Scene = forwardRef(({ rack, tooltipFields, onDeviceClick, onDeviceHover, onDeviceLeave }, ref) => {
// 从 Context 获取3D场景状态
const {
devices,
@@ -60,6 +96,18 @@ const Scene = ({ rack, tooltipFields, onDeviceClick, onDeviceHover, onDeviceLeav
deviceSlideEnabled
} = useScene3D();
// 用于存储 controls API
const controlsApiRef = useRef(null);
// 使用 useImperativeHandle 暴露重置方法给父组件
useImperativeHandle(ref, () => ({
resetView: () => {
if (controlsApiRef.current) {
controlsApiRef.current.reset();
}
}
}));
// 使用 useMemo 稳定 props 引用
const rackModelProps = useMemo(() => ({
rack,
@@ -72,47 +120,63 @@ const Scene = ({ rack, tooltipFields, onDeviceClick, onDeviceHover, onDeviceLeav
deviceSlideEnabled
}), [rack, devices, selectedDevice, onDeviceClick, onDeviceLeave, onDeviceHover, tooltipFields, deviceSlideEnabled]);
// 根据机柜高度动态计算相机初始位置
const rackHeight = rack?.height || 45;
const rackHeightMeters = rackHeight * 0.04445;
// 相机位置:确保能完整看到机柜,高度随机柜高度调整
const cameraPosition = useMemo(() => {
const baseHeight = 2;
const heightFactor = rackHeightMeters * 0.6;
const distance = Math.max(3, rackHeightMeters * 1.2);
return [distance * 0.7, baseHeight + heightFactor * 0.3, distance];
}, [rackHeightMeters]);
// 相机目标点(机柜中心)
const cameraTarget = useMemo(() => {
return [0, rackHeightMeters / 2 + 0.5, 0];
}, [rackHeightMeters]);
return (
<Canvas
shadows
dpr={deviceDpr}
performance={{ min: 0.5 }}
gl={{
antialias: !isMobile, // 移动端关闭抗锯齿提升性能
antialias: true, // 对所有设备开启抗锯齿提升清晰度
alpha: true, // 必须开启alpha以支持透明背景
powerPreference: 'high-performance'
}}
style={{ background: 'transparent' }}
>
<PerspectiveCamera makeDefault position={[3, 2, 4]} fov={50} />
<PerspectiveCamera makeDefault position={cameraPosition} fov={45} />
<ambientLight intensity={0.5} color="#ffffff" />
<pointLight position={[5, 8, 5]} intensity={2} color="#ffffff" castShadow />
<directionalLight
position={[10, 10, 5]}
intensity={1}
castShadow
shadow-mapSize={[1024, 1024]}
<directionalLight
position={[10, 10, 5]}
intensity={1}
castShadow
shadow-mapSize={[2048, 2048]}
shadow-camera-far={20}
shadow-camera-left={-10}
shadow-camera-right={10}
shadow-camera-top={10}
shadow-camera-bottom={-10}
/>
<Suspense fallback={null}>
<Environment files={envMapUrl} blur={0.5} resolution={256} background={false} />
</Suspense>
{/* Models */}
<group position={[0, 0, 0]}>
<RackModel {...rackModelProps} />
</group>
{/* Controls - 使用独立组件保持旋转中心固定 */}
<Controls rack={rack} />
<Controls rack={rack} onControlsReady={(api) => { controlsApiRef.current = api; }} />
</Canvas>
);
};
});
export default Scene;