|
|
@@ -4,6 +4,7 @@ import { Clock } from 'lucide-react';
|
|
|
import { slotApi, LockCabinetSlotVO, SlotPageParam } from '../../api/lockCabinet/slots';
|
|
|
import { lockCabinetApi, LockCabinetVO } from '../../api/lockCabinet';
|
|
|
import { dateFormatter } from '../../utils/formatTime';
|
|
|
+import './LockCabinet3D.css';
|
|
|
|
|
|
interface MapDataProps {
|
|
|
cabinetId: string;
|
|
|
@@ -11,6 +12,7 @@ interface MapDataProps {
|
|
|
|
|
|
export default function MapData({ cabinetId }: MapDataProps) {
|
|
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
|
+ const sceneHostRef = useRef<HTMLDivElement>(null);
|
|
|
const [cabinetInfo, setCabinetInfo] = useState<LockCabinetVO | null>(null);
|
|
|
const [slotData, setSlotData] = useState<LockCabinetSlotVO[]>([]);
|
|
|
const [updateTime, setUpdateTime] = useState<string | null>(null);
|
|
|
@@ -21,7 +23,9 @@ export default function MapData({ cabinetId }: MapDataProps) {
|
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
|
const previousMousePosition = useRef({ x: 0, y: 0 });
|
|
|
const rotation = useRef({ x: -5, y: -25 });
|
|
|
- const scale = useRef(1);
|
|
|
+ // 用户缩放(在自适应 fitScale 的基础上)
|
|
|
+ const userScale = useRef(1);
|
|
|
+ const [fitScale, setFitScale] = useState(1);
|
|
|
|
|
|
// 统计数据
|
|
|
const stats = React.useMemo(() => {
|
|
|
@@ -91,12 +95,13 @@ export default function MapData({ cabinetId }: MapDataProps) {
|
|
|
rotation.current.x -= deltaY * 0.5;
|
|
|
rotation.current.x = Math.max(-45, Math.min(45, rotation.current.x));
|
|
|
|
|
|
+ const effectiveScale = fitScale * userScale.current;
|
|
|
if (wrapperRef.current) {
|
|
|
- wrapperRef.current.style.transform = `rotateX(${rotation.current.x}deg) rotateY(${rotation.current.y}deg) scale(${scale.current})`;
|
|
|
+ wrapperRef.current.style.transform = `rotateX(${rotation.current.x}deg) rotateY(${rotation.current.y}deg) scale(${effectiveScale})`;
|
|
|
}
|
|
|
|
|
|
previousMousePosition.current = { x: e.clientX, y: e.clientY };
|
|
|
- }, [isDragging]);
|
|
|
+ }, [isDragging, fitScale]);
|
|
|
|
|
|
const handleMouseUp = useCallback(() => {
|
|
|
setIsDragging(false);
|
|
|
@@ -105,12 +110,14 @@ export default function MapData({ cabinetId }: MapDataProps) {
|
|
|
// 滚轮缩放
|
|
|
const handleWheel = useCallback((e: React.WheelEvent) => {
|
|
|
e.preventDefault();
|
|
|
- scale.current += e.deltaY * -0.001;
|
|
|
- scale.current = Math.max(0.3, Math.min(1.5, scale.current));
|
|
|
+ // 为确保“柜子高度不超过一屏”,这里限制最大只能缩放到 fitScale(即 userScale <= 1)
|
|
|
+ userScale.current += e.deltaY * -0.001;
|
|
|
+ userScale.current = Math.max(0.3, Math.min(1, userScale.current));
|
|
|
+ const effectiveScale = fitScale * userScale.current;
|
|
|
if (wrapperRef.current) {
|
|
|
- wrapperRef.current.style.transform = `rotateX(${rotation.current.x}deg) rotateY(${rotation.current.y}deg) scale(${scale.current})`;
|
|
|
+ wrapperRef.current.style.transform = `rotateX(${rotation.current.x}deg) rotateY(${rotation.current.y}deg) scale(${effectiveScale})`;
|
|
|
}
|
|
|
- }, []);
|
|
|
+ }, [fitScale]);
|
|
|
|
|
|
// 触摸支持
|
|
|
const touchStartDistance = useRef(0);
|
|
|
@@ -125,7 +132,7 @@ export default function MapData({ cabinetId }: MapDataProps) {
|
|
|
e.touches[0].clientX - e.touches[1].clientX,
|
|
|
e.touches[0].clientY - e.touches[1].clientY
|
|
|
);
|
|
|
- initialScale.current = scale.current;
|
|
|
+ initialScale.current = userScale.current;
|
|
|
}
|
|
|
}, []);
|
|
|
|
|
|
@@ -140,7 +147,8 @@ export default function MapData({ cabinetId }: MapDataProps) {
|
|
|
rotation.current.x = Math.max(-45, Math.min(45, rotation.current.x));
|
|
|
|
|
|
if (wrapperRef.current) {
|
|
|
- wrapperRef.current.style.transform = `rotateX(${rotation.current.x}deg) rotateY(${rotation.current.y}deg) scale(${scale.current})`;
|
|
|
+ const effectiveScale = fitScale * userScale.current;
|
|
|
+ wrapperRef.current.style.transform = `rotateX(${rotation.current.x}deg) rotateY(${rotation.current.y}deg) scale(${effectiveScale})`;
|
|
|
}
|
|
|
previousMousePosition.current = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
|
} else if (e.touches.length === 2) {
|
|
|
@@ -148,13 +156,15 @@ export default function MapData({ cabinetId }: MapDataProps) {
|
|
|
e.touches[0].clientX - e.touches[1].clientX,
|
|
|
e.touches[0].clientY - e.touches[1].clientY
|
|
|
);
|
|
|
- scale.current = initialScale.current * (currentDistance / touchStartDistance.current);
|
|
|
- scale.current = Math.max(0.3, Math.min(1.5, scale.current));
|
|
|
+ userScale.current = initialScale.current * (currentDistance / touchStartDistance.current);
|
|
|
+ // 同样限制最大只能到 1(不超过 fitScale)
|
|
|
+ userScale.current = Math.max(0.3, Math.min(1, userScale.current));
|
|
|
if (wrapperRef.current) {
|
|
|
- wrapperRef.current.style.transform = `rotateX(${rotation.current.x}deg) rotateY(${rotation.current.y}deg) scale(${scale.current})`;
|
|
|
+ const effectiveScale = fitScale * userScale.current;
|
|
|
+ wrapperRef.current.style.transform = `rotateX(${rotation.current.x}deg) rotateY(${rotation.current.y}deg) scale(${effectiveScale})`;
|
|
|
}
|
|
|
}
|
|
|
- }, [isDragging]);
|
|
|
+ }, [isDragging, fitScale]);
|
|
|
|
|
|
const handleTouchEnd = useCallback(() => {
|
|
|
setIsDragging(false);
|
|
|
@@ -179,6 +189,36 @@ export default function MapData({ cabinetId }: MapDataProps) {
|
|
|
}
|
|
|
}, [cabinetId]);
|
|
|
|
|
|
+ // 自适应缩放:保证柜子整体高度不超过可视区域(左侧容器)
|
|
|
+ useEffect(() => {
|
|
|
+ if (!sceneHostRef.current) return;
|
|
|
+ const host = sceneHostRef.current;
|
|
|
+
|
|
|
+ const BASE_W = 800;
|
|
|
+ const BASE_H = 1100;
|
|
|
+
|
|
|
+ const update = () => {
|
|
|
+ const rect = host.getBoundingClientRect();
|
|
|
+ if (!rect.width || !rect.height) return;
|
|
|
+ const nextFit = Math.min(rect.width / BASE_W, rect.height / BASE_H);
|
|
|
+ // 允许放大(容器更大时),但在常见 1 屏高度下基本都会 <= 1
|
|
|
+ const clamped = Math.max(0.2, Math.min(2, nextFit));
|
|
|
+ setFitScale(clamped);
|
|
|
+
|
|
|
+ // 立即同步当前 transform(避免等待下一次交互)
|
|
|
+ const effectiveScale = clamped * userScale.current;
|
|
|
+ if (wrapperRef.current) {
|
|
|
+ wrapperRef.current.style.transform = `rotateX(${rotation.current.x}deg) rotateY(${rotation.current.y}deg) scale(${effectiveScale})`;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ update();
|
|
|
+ const ro = new ResizeObserver(() => update());
|
|
|
+ ro.observe(host);
|
|
|
+
|
|
|
+ return () => ro.disconnect();
|
|
|
+ }, []);
|
|
|
+
|
|
|
// 按行分组槽位数据(用于 3D 显示)
|
|
|
const groupedSlots = React.useMemo(() => {
|
|
|
const grouped: Record<number, LockCabinetSlotVO[]> = {};
|
|
|
@@ -209,9 +249,9 @@ export default function MapData({ cabinetId }: MapDataProps) {
|
|
|
<div className="flex gap-6 h-full min-h-0">
|
|
|
{/* 左侧:3D 机柜 */}
|
|
|
<div className="flex-1 min-h-0 bg-white rounded-lg shadow-sm p-4 overflow-hidden relative">
|
|
|
- <div
|
|
|
- className="w-full h-full flex items-center justify-center"
|
|
|
- style={{ perspective: '2000px', perspectiveOrigin: '50% 50%' }}
|
|
|
+ <div
|
|
|
+ ref={sceneHostRef}
|
|
|
+ className="w-full h-full flex items-center justify-center select-none touch-none"
|
|
|
onMouseDown={handleMouseDown}
|
|
|
onMouseMove={handleMouseMove}
|
|
|
onMouseUp={handleMouseUp}
|
|
|
@@ -221,307 +261,179 @@ export default function MapData({ cabinetId }: MapDataProps) {
|
|
|
onTouchMove={handleTouchMove}
|
|
|
onTouchEnd={handleTouchEnd}
|
|
|
>
|
|
|
- <div
|
|
|
- ref={wrapperRef}
|
|
|
- className="relative"
|
|
|
- style={{
|
|
|
- transformStyle: 'preserve-3d',
|
|
|
- transform: `rotateX(${rotation.current.x}deg) rotateY(${rotation.current.y}deg) scale(${scale.current})`,
|
|
|
- transition: 'transform 0.1s ease-out',
|
|
|
- }}
|
|
|
- >
|
|
|
- {/* 地面阴影 */}
|
|
|
- <div
|
|
|
- className="absolute"
|
|
|
- style={{
|
|
|
- left: '50%',
|
|
|
- top: '50%',
|
|
|
- transform: 'translateX(-50%) translateY(calc(325px + 60px)) rotateX(90deg)',
|
|
|
- width: '520px',
|
|
|
- height: '240px',
|
|
|
- background: 'radial-gradient(ellipse at center, rgba(0,0,0,0.2) 0%, transparent 65%)',
|
|
|
- filter: 'blur(20px)',
|
|
|
- }}
|
|
|
- />
|
|
|
-
|
|
|
- {/* 3D 机柜容器 */}
|
|
|
- <div
|
|
|
- className="relative"
|
|
|
- style={{
|
|
|
- width: '360px',
|
|
|
- height: '650px',
|
|
|
- transformStyle: 'preserve-3d',
|
|
|
- }}
|
|
|
- >
|
|
|
- {/* 背面 */}
|
|
|
- <div
|
|
|
- className="absolute"
|
|
|
- style={{
|
|
|
- width: '360px',
|
|
|
- height: '650px',
|
|
|
- background: 'linear-gradient(180deg, #808080 0%, #707070 50%, #606060 100%)',
|
|
|
- borderRadius: '8px',
|
|
|
- border: '3px solid #555',
|
|
|
- transform: 'translateZ(-150px)',
|
|
|
- }}
|
|
|
- />
|
|
|
-
|
|
|
- {/* 左侧面 */}
|
|
|
+ <div className="lc3dRoot">
|
|
|
+ <div className="lc3dScene">
|
|
|
<div
|
|
|
- className="absolute"
|
|
|
+ ref={wrapperRef}
|
|
|
+ className="lc3dCabinetWrapper"
|
|
|
style={{
|
|
|
- width: '150px',
|
|
|
- height: '650px',
|
|
|
- background: 'linear-gradient(90deg, #989898 0%, #a8a8a8 30%, #b8b8b8 70%, #c8c8c8 100%)',
|
|
|
- left: '-150px',
|
|
|
- top: '0',
|
|
|
- transformOrigin: 'right center',
|
|
|
- transform: 'rotateY(-90deg)',
|
|
|
- borderTop: '3px solid #b0b0b0',
|
|
|
- borderBottom: '3px solid #808080',
|
|
|
- borderLeft: '3px solid #888',
|
|
|
+ transform: `rotateX(${rotation.current.x}deg) rotateY(${rotation.current.y}deg) scale(${fitScale * userScale.current})`,
|
|
|
}}
|
|
|
- />
|
|
|
-
|
|
|
- {/* 右侧面 */}
|
|
|
- <div
|
|
|
- className="absolute"
|
|
|
- style={{
|
|
|
- width: '150px',
|
|
|
- height: '650px',
|
|
|
- background: 'linear-gradient(270deg, #888888 0%, #989898 30%, #a8a8a8 70%, #b0b0b0 100%)',
|
|
|
- left: '360px',
|
|
|
- top: '0',
|
|
|
- transformOrigin: 'left center',
|
|
|
- transform: 'rotateY(90deg)',
|
|
|
- borderTop: '3px solid #a0a0a0',
|
|
|
- borderBottom: '3px solid #777',
|
|
|
- borderRight: '3px solid #777',
|
|
|
- }}
|
|
|
- />
|
|
|
-
|
|
|
- {/* 顶部 */}
|
|
|
- <div
|
|
|
- className="absolute"
|
|
|
- style={{
|
|
|
- width: '360px',
|
|
|
- height: '150px',
|
|
|
- background: 'linear-gradient(180deg, #d0d0d0 0%, #c8c8c8 30%, #c0c0c0 70%, #b8b8b8 100%)',
|
|
|
- left: '0',
|
|
|
- top: '-150px',
|
|
|
- transformOrigin: 'center bottom',
|
|
|
- transform: 'rotateX(90deg)',
|
|
|
- borderLeft: '3px solid #b8b8b8',
|
|
|
- borderRight: '3px solid #a8a8a8',
|
|
|
- borderTop: '3px solid #909090',
|
|
|
- }}
|
|
|
- />
|
|
|
+ >
|
|
|
+ {/* 地面阴影 */}
|
|
|
+ <div className="lc3dGroundShadow" />
|
|
|
+
|
|
|
+ {/* 3D 盒子 */}
|
|
|
+ <div className="lc3dCabinet3d">
|
|
|
+ {/* 背面 */}
|
|
|
+ <div className="lc3dCabinetBack" />
|
|
|
+ {/* 左侧面 */}
|
|
|
+ <div className="lc3dCabinetLeft" />
|
|
|
+ {/* 右侧面 */}
|
|
|
+ <div className="lc3dCabinetRight" />
|
|
|
+ {/* 顶部 */}
|
|
|
+ <div className="lc3dCabinetTop" />
|
|
|
+ {/* 底部 */}
|
|
|
+ <div className="lc3dCabinetBottom" />
|
|
|
+
|
|
|
+ {/* 正面(包含所有内容) */}
|
|
|
+ <div className="lc3dCabinetFront">
|
|
|
+ {/* 顶部挂钩 */}
|
|
|
+ <div className="lc3dHook lc3dHookLeft" />
|
|
|
+ <div className="lc3dHook lc3dHookRight" />
|
|
|
+
|
|
|
+ {/* 左侧螺丝 */}
|
|
|
+ <div className="lc3dScrewsLeft">
|
|
|
+ {Array.from({ length: 14 }).map((_, i) => (
|
|
|
+ <div key={i} className="lc3dScrew" />
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
|
|
|
- {/* 底部 */}
|
|
|
- <div
|
|
|
- className="absolute"
|
|
|
- style={{
|
|
|
- width: '360px',
|
|
|
- height: '150px',
|
|
|
- background: 'linear-gradient(0deg, #606060 0%, #707070 30%, #787878 70%, #808080 100%)',
|
|
|
- left: '0',
|
|
|
- top: '650px',
|
|
|
- transformOrigin: 'center top',
|
|
|
- transform: 'rotateX(-90deg)',
|
|
|
- borderLeft: '3px solid #707070',
|
|
|
- borderRight: '3px solid #606060',
|
|
|
- borderBottom: '3px solid #505050',
|
|
|
- }}
|
|
|
- />
|
|
|
+ {/* 右侧螺丝 */}
|
|
|
+ <div className="lc3dScrewsRight">
|
|
|
+ {Array.from({ length: 14 }).map((_, i) => (
|
|
|
+ <div key={i} className="lc3dScrew" />
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
|
|
|
- {/* 正面 */}
|
|
|
- <div
|
|
|
- className="absolute"
|
|
|
- style={{
|
|
|
- width: '360px',
|
|
|
- height: '650px',
|
|
|
- background: 'linear-gradient(180deg, #d8d8d8 0%, #c8c8c8 50%, #b8b8b8 100%)',
|
|
|
- borderRadius: '8px',
|
|
|
- border: '3px solid #a0a0a0',
|
|
|
- boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.5), inset 0 -1px 0 rgba(0,0,0,0.2)',
|
|
|
- transform: 'translateZ(0)',
|
|
|
- }}
|
|
|
- >
|
|
|
- {/* 顶部挂钩 */}
|
|
|
- <div
|
|
|
- className="absolute"
|
|
|
- style={{
|
|
|
- top: '-15px',
|
|
|
- left: '30px',
|
|
|
- width: '30px',
|
|
|
- height: '25px',
|
|
|
- background: 'linear-gradient(180deg, #999 0%, #666 100%)',
|
|
|
- borderRadius: '50% 50% 0 0',
|
|
|
- transform: 'translateZ(5px)',
|
|
|
- }}
|
|
|
- />
|
|
|
- <div
|
|
|
- className="absolute"
|
|
|
- style={{
|
|
|
- top: '-15px',
|
|
|
- right: '30px',
|
|
|
- width: '30px',
|
|
|
- height: '25px',
|
|
|
- background: 'linear-gradient(180deg, #999 0%, #666 100%)',
|
|
|
- borderRadius: '50% 50% 0 0',
|
|
|
- transform: 'translateZ(5px)',
|
|
|
- }}
|
|
|
- />
|
|
|
-
|
|
|
- {/* 头部区域 */}
|
|
|
- <div className="relative z-10 px-5 pt-4 pb-2 flex justify-between items-center">
|
|
|
- <div className="flex items-center gap-2">
|
|
|
- <div className="w-12 h-10 relative">
|
|
|
- <svg viewBox="0 0 50 40" className="w-full h-full" style={{ filter: 'drop-shadow(2px 2px 2px rgba(0,0,0,0.3))' }}>
|
|
|
- <path d="M5 20 Q5 8 20 8 L35 8 Q45 8 45 18 Q45 28 35 28 L25 28 Q20 28 20 33 Q20 38 25 38"
|
|
|
- stroke="#c41e3a" strokeWidth="4" fill="none" strokeLinecap="round"/>
|
|
|
- <circle cx="25" cy="38" r="3" fill="#c41e3a"/>
|
|
|
- </svg>
|
|
|
+ {/* 头部 */}
|
|
|
+ <div className="lc3dHeader">
|
|
|
+ <div className="lc3dLogo">
|
|
|
+ <div className="lc3dLogoIcon">
|
|
|
+ <svg viewBox="0 0 50 40">
|
|
|
+ <path
|
|
|
+ d="M5 20 Q5 8 20 8 L35 8 Q45 8 45 18 Q45 28 35 28 L25 28 Q20 28 20 33 Q20 38 25 38"
|
|
|
+ stroke="#c41e3a"
|
|
|
+ strokeWidth="4"
|
|
|
+ fill="none"
|
|
|
+ strokeLinecap="round"
|
|
|
+ />
|
|
|
+ <circle cx="25" cy="38" r="3" fill="#c41e3a" />
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <span className="lc3dLogoText">BOZZYS</span>
|
|
|
+ </div>
|
|
|
+ <div className="lc3dTitle">LOTO锁控系统</div>
|
|
|
</div>
|
|
|
- <span className="text-red-600 text-xl font-bold tracking-wider" style={{ textShadow: '2px 2px 4px rgba(0,0,0,0.2)' }}>
|
|
|
- BOZZYS
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- <div className="text-red-600 text-lg font-bold" style={{ textShadow: '2px 2px 4px rgba(0,0,0,0.2)' }}>
|
|
|
- LOTO锁控系统
|
|
|
- </div>
|
|
|
- </div>
|
|
|
|
|
|
- {/* 主面板 */}
|
|
|
- <div className="mx-4 mt-2 mb-2 rounded bg-gradient-to-b from-gray-200 to-gray-300 p-4 shadow-inner" style={{ transform: 'translateZ(5px)' }}>
|
|
|
- {/* 屏幕区域 */}
|
|
|
- <div className="flex justify-center mb-4">
|
|
|
- <div
|
|
|
- className="relative rounded-lg p-4 pb-11"
|
|
|
- style={{
|
|
|
- background: 'linear-gradient(180deg, #2a2a2a 0%, #1a1a1a 50%, #0a0a0a 100%)',
|
|
|
- boxShadow: '0 6px 20px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.1), inset 0 -1px 0 rgba(0,0,0,0.3)',
|
|
|
- transform: 'translateZ(8px)',
|
|
|
- border: '2px solid #333',
|
|
|
- }}
|
|
|
- >
|
|
|
- {/* 屏幕 */}
|
|
|
- <div
|
|
|
- className="rounded border-2 border-gray-600 overflow-hidden"
|
|
|
- style={{
|
|
|
- width: '320px',
|
|
|
- height: '180px',
|
|
|
- background: '#1a3a5c',
|
|
|
- boxShadow: '0 2px 8px rgba(0,0,0,0.4), inset 0 0 30px rgba(0,100,200,0.3)',
|
|
|
- }}
|
|
|
- >
|
|
|
- <div className="w-full h-full bg-gradient-to-b from-blue-900 to-blue-800 p-2">
|
|
|
- <div className="flex justify-between text-white text-xs mb-2">
|
|
|
- <span>设备状态监控</span>
|
|
|
- <span>{new Date().toLocaleString('zh-CN')}</span>
|
|
|
- </div>
|
|
|
- <div className="bg-white/10 rounded p-1">
|
|
|
- {slotData.slice(0, 5).map((slot, idx) => (
|
|
|
- <div key={idx} className="flex justify-between text-cyan-400 text-xs py-1 px-1 border-b border-white/10 last:border-0">
|
|
|
- <span>{slot.slotName || slot.slotCode || idx + 1}</span>
|
|
|
- <span style={{ color: slot.status === '1' ? '#ff4444' : slot.isOccupied === '1' ? '#44ff44' : '#ffff44' }}>
|
|
|
- {getSlotStatus(slot)}
|
|
|
- </span>
|
|
|
- <span>{slot.slotName || '-'}</span>
|
|
|
- <span>{slot.updateTime ? new Date(slot.updateTime).toLocaleTimeString('zh-CN') : '-'}</span>
|
|
|
+ {/* 主面板 */}
|
|
|
+ <div className="lc3dMainPanel">
|
|
|
+ {/* 屏幕区域 */}
|
|
|
+ <div className="lc3dScreenSection">
|
|
|
+ <div className="lc3dMonitorFrame">
|
|
|
+ <div className="lc3dScreen">
|
|
|
+ <div className="lc3dScreenContent">
|
|
|
+ <div className="lc3dScreenHeader">
|
|
|
+ <span>设备状态监控</span>
|
|
|
+ <span>{new Date().toLocaleString('zh-CN')}</span>
|
|
|
+ </div>
|
|
|
+ <div className="lc3dScreenTable">
|
|
|
+ <div className="lc3dScreenRow">
|
|
|
+ <span>编号</span>
|
|
|
+ <span>状态</span>
|
|
|
+ <span>用户</span>
|
|
|
+ <span>时间</span>
|
|
|
+ </div>
|
|
|
+ {slotData.slice(0, 4).map((slot, idx) => (
|
|
|
+ <div key={idx} className="lc3dScreenRow">
|
|
|
+ <span>{slot.slotName || slot.slotCode || String(idx + 1).padStart(3, '0')}</span>
|
|
|
+ <span style={{ color: slot.status === '1' ? '#ff4444' : slot.isOccupied === '1' ? '#44ff44' : '#ffff44' }}>
|
|
|
+ {slot.status === '1' ? '异常' : slot.isOccupied === '1' ? '借出' : '待归'}
|
|
|
+ </span>
|
|
|
+ <span>{slot.hardwareId ? String(slot.hardwareId) : '-'}</span>
|
|
|
+ <span>{slot.updateTime ? new Date(slot.updateTime).toLocaleTimeString('zh-CN') : '-'}</span>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ <div className="lc3dScreenButtons">
|
|
|
+ <button className={`lc3dScreenBtn lc3dScreenBtnRed`} type="button">
|
|
|
+ 紧急解锁
|
|
|
+ </button>
|
|
|
+ <button className={`lc3dScreenBtn lc3dScreenBtnGreen`} type="button">
|
|
|
+ 系统设置
|
|
|
+ </button>
|
|
|
</div>
|
|
|
- ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {/* 底部区域:刷卡和指纹 */}
|
|
|
+ <div className="lc3dMonitorBottomArea">
|
|
|
+ <div className="lc3dCardReader">
|
|
|
+ <div className="lc3dCardReaderIcon" />
|
|
|
+ </div>
|
|
|
+ <div className="lc3dFingerprintReader">
|
|
|
+ <div className="lc3dFingerprintIcon" />
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- {/* 底部刷卡和指纹 */}
|
|
|
- <div className="absolute bottom-2 right-3 flex items-center gap-2">
|
|
|
- <div className="w-12 h-7 rounded bg-gradient-to-b from-gray-600 to-gray-800 border border-gray-700 flex items-center justify-center">
|
|
|
- <div className="w-5 h-4 border border-gray-600 rounded bg-gradient-to-b from-gray-700 to-gray-900" />
|
|
|
- </div>
|
|
|
- <div className="w-7 h-7 rounded bg-gradient-to-b from-gray-600 to-gray-800 border border-gray-700 flex items-center justify-center">
|
|
|
- <div className="w-4 h-4 border-2 border-gray-600 rounded-full" />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
|
|
|
- {/* 抽屉 */}
|
|
|
- <div className="h-9 mb-2 rounded bg-gradient-to-b from-gray-100 to-gray-200 border border-gray-400 flex items-center justify-center shadow-md" style={{ transform: 'translateZ(12px)' }}>
|
|
|
- <div className="w-10 h-2 rounded bg-gradient-to-b from-gray-400 to-gray-600 shadow-sm" />
|
|
|
- </div>
|
|
|
+ {/* 抽屉 */}
|
|
|
+ <div className="lc3dDrawer">
|
|
|
+ <div className="lc3dDrawerHandle" />
|
|
|
+ </div>
|
|
|
|
|
|
- {/* 钥匙仓区域 */}
|
|
|
- <div className="py-4 border-b-2 border-gray-400" style={{ transform: 'translateZ(5px)' }}>
|
|
|
- <div className="text-center text-xs text-gray-600 font-bold mb-2">钥匙仓</div>
|
|
|
- <div className="flex justify-center gap-5">
|
|
|
- {keySlots.map((slot, idx) => (
|
|
|
- <div key={idx} className="relative">
|
|
|
- <span className="absolute -top-5 left-1/2 -translate-x-1/2 text-xs font-bold text-gray-600">{idx + 1}</span>
|
|
|
- <div
|
|
|
- className="w-12 h-12 rounded-full border-3 border-gray-600 flex items-center justify-center shadow-lg"
|
|
|
- style={{
|
|
|
- background: 'linear-gradient(180deg, #3a3a3a 0%, #1a1a1a 100%)',
|
|
|
- boxShadow: '0 4px 8px rgba(0,0,0,0.4), inset 0 2px 4px rgba(0,0,0,0.5)',
|
|
|
- transform: 'translateZ(10px)',
|
|
|
- }}
|
|
|
- >
|
|
|
- <div className="w-8 h-8 rounded-full border-2 border-gray-700 bg-gradient-to-b from-gray-800 to-black" />
|
|
|
- </div>
|
|
|
- <div
|
|
|
- className={`absolute -bottom-2 left-1/2 -translate-x-1/2 w-2 h-2 rounded-full ${
|
|
|
- slot.status === '1' ? 'bg-red-500' : slot.isOccupied === '1' ? 'bg-green-500' : 'bg-gray-500'
|
|
|
- }`}
|
|
|
- style={{
|
|
|
- boxShadow: slot.status === '1' ? '0 0 6px #ff0000' : slot.isOccupied === '1' ? '0 0 6px #00ff00' : 'none',
|
|
|
- }}
|
|
|
- />
|
|
|
+ {/* 钥匙仓区域 */}
|
|
|
+ <div className="lc3dKeySection">
|
|
|
+ <div className="lc3dKeySectionTitle">钥匙仓</div>
|
|
|
+ <div className="lc3dKeyRow">
|
|
|
+ {Array.from({ length: 4 }).map((_, idx) => {
|
|
|
+ const slot = keySlots[idx];
|
|
|
+ const isAbnormal = slot?.status === '1';
|
|
|
+ const isOccupied = slot?.isOccupied === '1';
|
|
|
+ const indicatorClass = isAbnormal
|
|
|
+ ? 'lc3dKeyIndicatorRed'
|
|
|
+ : isOccupied
|
|
|
+ ? 'lc3dKeyIndicatorRed'
|
|
|
+ : 'lc3dKeyIndicatorGreen';
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div key={idx} className="lc3dKeySlot">
|
|
|
+ <span className="lc3dKeyNumber">{idx + 1}</span>
|
|
|
+ <div className={`lc3dKeyIndicator ${indicatorClass}`} />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ })}
|
|
|
</div>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ </div>
|
|
|
|
|
|
- {/* 锁仓区域 */}
|
|
|
- <div className="py-2" style={{ transform: 'translateZ(3px)' }}>
|
|
|
- <div className="text-center text-xs text-gray-600 font-bold mb-2 mt-2">锁仓</div>
|
|
|
- {lockSlots.map((row, rowIdx) => (
|
|
|
- <div key={rowIdx} className="flex justify-center gap-1.5 mb-2 mt-4 first:mt-5">
|
|
|
- {row.map((slot, slotIdx) => (
|
|
|
- <div key={slotIdx} className="relative">
|
|
|
- <span className="absolute -top-4 left-1/2 -translate-x-1/2 text-xs font-bold text-gray-600">
|
|
|
- {rowIdx * 10 + slotIdx + 1}
|
|
|
- </span>
|
|
|
- <div
|
|
|
- className="w-9 h-11 rounded flex flex-col items-center p-1 border border-gray-600 shadow-md"
|
|
|
- style={{
|
|
|
- background: 'linear-gradient(180deg, #4a4a4a 0%, #2a2a2a 100%)',
|
|
|
- boxShadow: 'inset 0 2px 4px rgba(0,0,0,0.4), 0 2px 4px rgba(0,0,0,0.3)',
|
|
|
- transform: 'translateZ(8px)',
|
|
|
- }}
|
|
|
- >
|
|
|
- <div
|
|
|
- className={`w-2 h-2 rounded-full mb-1 ${
|
|
|
- slot.status === '1' ? 'bg-red-500' : slot.isOccupied === '1' ? 'bg-red-500' : 'bg-gray-600'
|
|
|
- }`}
|
|
|
- style={{
|
|
|
- boxShadow: slot.status === '1' || slot.isOccupied === '1' ? '0 0 6px #ff0000' : 'none',
|
|
|
- }}
|
|
|
- />
|
|
|
- <div className="w-5 h-6 rounded bg-gradient-to-b from-black to-gray-900 border border-gray-700 shadow-inner" />
|
|
|
- </div>
|
|
|
+ {/* 锁仓区域 */}
|
|
|
+ <div className="lc3dLockSection">
|
|
|
+ <div className="lc3dLockSectionTitle">锁仓</div>
|
|
|
+ {lockSlots.map((row, rowIdx) => (
|
|
|
+ <div key={rowIdx} className="lc3dLockRow">
|
|
|
+ {Array.from({ length: 10 }).map((_, slotIdx) => {
|
|
|
+ const slot = row[slotIdx];
|
|
|
+ const num = rowIdx * 10 + slotIdx + 1;
|
|
|
+ const isOn = slot?.status === '1' || slot?.isOccupied === '1';
|
|
|
+ return (
|
|
|
+ <div key={slotIdx} className="lc3dLockSlot">
|
|
|
+ <span className="lc3dLockNumber">{num}</span>
|
|
|
+ <div className={`lc3dLockIndicator ${isOn ? 'lc3dLockIndicatorRed' : 'lc3dLockIndicatorOff'}`} />
|
|
|
+ <div className="lc3dLockHole" />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ })}
|
|
|
</div>
|
|
|
))}
|
|
|
</div>
|
|
|
- ))}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ <div className="lc3dControlsHint">拖拽鼠标旋转 3D 视角 | 滚轮缩放</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div className="absolute bottom-3 left-1/2 -translate-x-1/2 text-xs text-gray-600 bg-white/70 backdrop-blur px-3 py-1 rounded-full border border-gray-200 pointer-events-none">
|
|
|
- 拖拽鼠标旋转 3D 视角 | 滚轮缩放
|
|
|
- </div>
|
|
|
</div>
|
|
|
|
|
|
{/* 右侧:详情信息 */}
|