فهرست منبع

修复bug和新增3d柜子

pm 3 ماه پیش
والد
کامیت
b2af108d2a

+ 0 - 13
src/Dashboard.tsx

@@ -10,7 +10,6 @@ import IsolationWork from './components/IsolationWork';
 import ProfileSettings from './components/ProfileSettings';
 import DashboardComponent from './components/Dashboard';
 import ExecutorDashboard from './components/ExecutorDashboard';
-import LockCabinetDetail from './components/lockCabinet/LockCabinetDetail';
 import NotificationManagement from './components/NotificationManagement';
 import FormManagement from './components/FormManagement';
 import MyTask from './components/MyTask';
@@ -745,11 +744,6 @@ export default function Dashboard() {
 
   // 监听路径变化,处理详情页显示和菜单状态更新
   useEffect(() => {
-    // 如果是详情页,设置 activeMenu 为空,直接显示详情页
-    if (location.pathname.startsWith('/lock-cabinet/detail')) {
-      setActiveMenu('');
-      return;
-    }
     
     // 如果路径是 /dashboard,检查是否有从其他页面跳转过来的菜单信息(navigateToMenu)
     // 注意:只有在有 navigateToMenu 时才恢复菜单状态,避免新登录用户读取到旧的 lastActiveMenu
@@ -933,11 +927,6 @@ export default function Dashboard() {
 
   // 根据URL路径初始化菜单状态(仅在首次加载时执行)
   useEffect(() => {
-    // 如果是详情页,设置 activeMenu 为空,直接显示详情页
-    if (location.pathname.startsWith('/lock-cabinet/detail')) {
-      setActiveMenu('');
-      return;
-    }
     
     // 检查是否有从其他页面跳转过来的菜单信息
     const navigateToMenu = sessionStorage.getItem('navigateToMenu');
@@ -1457,8 +1446,6 @@ export default function Dashboard() {
         <div className="flex-1 px-6 pb-6 overflow-auto">
         {showProfileSettings ? (
           <ProfileSettings onBack={() => setShowProfileSettings(false)} />
-        ) : location.pathname.startsWith('/lock-cabinet/detail') && (!activeMenu || activeMenu === '') ? (
-          <LockCabinetDetail />
         ) : activeMenu === 'dashboard' ? (
           hasRole('super_admin') ? <DashboardComponent /> : <ExecutorDashboard />
         ) : activeMenu === 'systemConfig' ? (

+ 4 - 45
src/components/SegregationPointForm.tsx

@@ -1,7 +1,5 @@
 import React, { useState, useImperativeHandle, forwardRef, useEffect } from 'react';
-import { Modal, Form, Input, Select, Upload, Image, Row, Col, message } from 'antd';
-import { PlusOutlined, UploadOutlined } from '@ant-design/icons';
-import type { UploadFile } from 'antd/es/upload/interface';
+import { Modal, Form, Input, Select, Row, Col, message } from 'antd';
 import { segregationPointApi, SegregationPointVO } from '../api/spm/index';
 import { postApi, PostVO } from '../api/Post';
 import { lotoStationApi } from '../api/lotoStation/index';
@@ -9,6 +7,7 @@ import { loginApi } from '../api/Login';
 import { getStrDictOptions, DICT_TYPE } from '../utils/dict';
 import { toast } from 'sonner';
 import { useTranslation } from 'react-i18next';
+import UploadImg from './lockCabinet/UploadImg';
 
 interface SegregationPointFormProps {
   onSuccess?: () => void;
@@ -283,32 +282,7 @@ const SegregationPointForm = forwardRef<SegregationPointFormRef, SegregationPoin
       }
     };
 
-    // 图片上传配置
-    const uploadProps = {
-      name: 'file',
-      action: '/api/upload', // 需要根据实际接口调整
-      listType: 'picture-card' as const,
-      maxCount: 1,
-      beforeUpload: (file: File) => {
-        const isImage = file.type.startsWith('image/');
-        if (!isImage) {
-          message.error(t('form.onlyImageAllowed'));
-          return false;
-        }
-        const isLt2M = file.size / 1024 / 1024 < 2;
-        if (!isLt2M) {
-          message.error(t('form.imageSizeLimit'));
-          return false;
-        }
-        return false; // 阻止自动上传,需要手动处理
-      },
-      onChange: (info: any) => {
-        if (info.file.status === 'done') {
-          // 上传成功,设置图片URL
-          form.setFieldsValue({ pointPicture: info.file.response?.url || info.file.url });
-        }
-      },
-    };
+    // 图片上传:使用通用上传组件(内部会调用 /infra/file/upload)
 
     return (
       <Modal
@@ -434,22 +408,7 @@ const SegregationPointForm = forwardRef<SegregationPointFormRef, SegregationPoin
                 label={t('form.segregationPointImage')}
                 name="pointPicture"
               >
-                <Upload {...uploadProps}>
-                  {form.getFieldValue('pointPicture') ? (
-                    <Image
-                      src={form.getFieldValue('pointPicture')}
-                      width={100}
-                      height={100}
-                      style={{ objectFit: 'cover' }}
-                      preview={false}
-                    />
-                  ) : (
-                    <div>
-                      <PlusOutlined />
-                      <div style={{ marginTop: 8 }}>{t('common.upload')}</div>
-                    </div>
-                  )}
-                </Upload>
+                <UploadImg width="100px" height="100px" />
               </Form.Item>
             </Col>
           </Row>

+ 46 - 22
src/components/lockCabinet/LockCabinetDetail.tsx

@@ -1,18 +1,14 @@
 import React, { useState } from 'react';
 import { useSearchParams, useNavigate } from 'react-router-dom';
-import { Radio } from 'antd';
-import { ArrowLeft } from 'lucide-react';
+import { ArrowLeft, RefreshCw } from 'lucide-react';
 import { Button } from '../ui/button';
-import SlotsList from './SlotsList';
 import MapData from './MapData';
 
 export default function LockCabinetDetail() {
   const [searchParams] = useSearchParams();
   const navigate = useNavigate();
   const cabinetId = searchParams.get('cabinetId') || '';
-  const [tabPosition, setTabPosition] = useState<'first' | 'second'>('first');
-
-  const currentComponent = tabPosition === 'first' ? <MapData cabinetId={cabinetId} /> : <SlotsList cabinetId={cabinetId} />;
+  const [refreshKey, setRefreshKey] = useState(0);
 
   const handleBack = () => {
     // 读取来源菜单信息,如果存在则使用它,否则默认使用硬件管理 -> 机柜
@@ -38,24 +34,52 @@ export default function LockCabinetDetail() {
     navigate('/dashboard');
   };
 
+  const handleRefresh = () => {
+    // 通过改变 key 来强制刷新 MapData 组件
+    setRefreshKey(prev => prev + 1);
+  };
+
   return (
-    <div className="p-4">
-      <div className="mb-4 flex items-center justify-between">
-        <Button
-          variant="ghost"
-          onClick={handleBack}
-          className="flex items-center gap-2"
-        >
-          <ArrowLeft className="w-4 h-4" />
-          返回机柜列表
-        </Button>
-        <Radio.Group value={tabPosition} onChange={(e) => setTabPosition(e.target.value)}>
-          <Radio.Button value="first">锁柜视图</Radio.Button>
-          <Radio.Button value="second">列表视图</Radio.Button>
-        </Radio.Group>
+    <div className="h-screen flex flex-col overflow-hidden bg-gradient-to-br from-gray-50 via-blue-50/30 to-gray-50">
+      {/* 顶部导航栏 */}
+      <nav className="bg-white/80 backdrop-blur-xl border-b border-gray-200/50 shadow-sm sticky top-0 z-50">
+        <div className="px-6 py-4">
+          <div className="flex items-center justify-between">
+            {/* 左侧:标题文字 */}
+            <div className="flex items-center">
+              <h1 className="text-lg text-gray-900 font-semibold">锁控柜可视化管理</h1>
+            </div>
+
+            {/* 右侧:刷新和返回按钮 */}
+            <div className="flex items-center gap-3">
+              <Button
+                onClick={handleRefresh}
+                variant="ghost"
+                className="flex items-center gap-2 hover:opacity-90"
+                style={{ backgroundColor: '#2563eb', color: '#fff' }}
+              >
+                <RefreshCw className="w-4 h-4" />
+                刷新
+              </Button>
+              <Button
+                variant="ghost"
+                onClick={handleBack}
+                className="flex items-center gap-2 hover:bg-gray-100"
+              >
+                <ArrowLeft className="w-4 h-4" />
+                返回
+              </Button>
+            </div>
+          </div>
+        </div>
+      </nav>
+
+      {/* 主内容区域 */}
+      <div className="flex-1 min-h-0 overflow-hidden p-4">
+        <div className="h-full min-h-0">
+          <MapData key={refreshKey} cabinetId={cabinetId} />
+        </div>
       </div>
-      {currentComponent}
     </div>
   );
 }
-

+ 597 - 186
src/components/lockCabinet/MapData.tsx

@@ -1,8 +1,8 @@
-import React, { useState, useEffect, useRef } from 'react';
+import React, { useState, useEffect, useRef, useCallback } from 'react';
 import { Modal, message } from 'antd';
 import { Clock } from 'lucide-react';
 import { slotApi, LockCabinetSlotVO, SlotPageParam } from '../../api/lockCabinet/slots';
-import { systemAttributeApi } from '../../api/systemAttribute';
+import { lockCabinetApi, LockCabinetVO } from '../../api/lockCabinet';
 import { dateFormatter } from '../../utils/formatTime';
 
 interface MapDataProps {
@@ -10,15 +10,49 @@ interface MapDataProps {
 }
 
 export default function MapData({ cabinetId }: MapDataProps) {
-  const canvasRef = useRef<HTMLCanvasElement>(null);
+  const wrapperRef = useRef<HTMLDivElement>(null);
+  const [cabinetInfo, setCabinetInfo] = useState<LockCabinetVO | null>(null);
   const [slotData, setSlotData] = useState<LockCabinetSlotVO[]>([]);
   const [updateTime, setUpdateTime] = useState<string | null>(null);
   const [dialogVisible, setDialogVisible] = useState(false);
   const [errorInfo, setErrorInfo] = useState('');
-  const [cachedImages, setCachedImages] = useState<Record<string, HTMLImageElement>>({});
-  const [cachedResults, setCachedResults] = useState<Record<string, string>>({});
+  
+  // 3D 控制状态
+  const [isDragging, setIsDragging] = useState(false);
+  const previousMousePosition = useRef({ x: 0, y: 0 });
+  const rotation = useRef({ x: -5, y: -25 });
+  const scale = useRef(1);
+
+  // 统计数据
+  const stats = React.useMemo(() => {
+    const total = slotData.length;
+    const occupied = slotData.filter(s => s.isOccupied === '1').length;
+    const free = total - occupied;
+    const abnormal = slotData.filter(s => s.status === '1').length;
+    return { total, occupied, free, abnormal };
+  }, [slotData]);
+
+  // 右侧两块:随机展示(每次刷新/重新进入会变化)
+  const randomRightStats = React.useMemo(() => {
+    const total = stats.total > 0 ? stats.total : 50;
+    const occupied = Math.floor(Math.random() * (total + 1));
+    const abnormal = Math.floor(Math.random() * (occupied + 1));
+    return { occupied, abnormal };
+  }, [stats.total]);
+
+  // 获取机柜详情
+  const getCabinetInfo = async () => {
+    if (!cabinetId) return;
+    try {
+      const info = await lockCabinetApi.selectIsLockCabinetById(Number(cabinetId));
+      setCabinetInfo(info);
+    } catch (error: any) {
+      console.error('获取机柜信息失败:', error);
+      message.error(error.message || '获取机柜信息失败');
+    }
+  };
 
-  // 获取数据
+  // 获取槽位数据
   const getData = async () => {
     if (!cabinetId) return;
 
@@ -35,213 +69,591 @@ export default function MapData({ cabinetId }: MapDataProps) {
       if (slots.length > 0 && slots[0].createTime) {
         setUpdateTime(dateFormatter(slots[0].createTime));
       }
-
-      // 获取图标配置
-      const icons = [
-        'icon.locker.normal',
-        'icon.locker.out',
-        'icon.padlock.normal',
-        'icon.padlock.out',
-        'icon.locker.exception'
-      ];
-
-      const results: Record<string, string> = {};
-      for (const key of icons) {
-        try {
-          const attr = await systemAttributeApi.getIsSystemAttributeByKey(key);
-          results[key] = attr.sysAttrValue || '';
-        } catch (error) {
-          console.error(`获取图标配置失败: ${key}`, error);
-        }
-      }
-      setCachedResults(results);
-
-      // 预加载图片
-      await preloadImages(results);
     } catch (error: any) {
       console.error('获取数据失败:', error);
       message.error(error.message || '获取数据失败');
     }
   };
 
-  // 预加载图片
-  const preloadImages = async (results: Record<string, string>) => {
-    const urls = Object.values(results).filter(url => url);
-    const images: Record<string, HTMLImageElement> = {};
-
-    for (const url of urls) {
-      if (cachedImages[url]) {
-        images[url] = cachedImages[url];
-      } else {
-        try {
-          const img = await loadImage(url);
-          images[url] = img;
-        } catch (error) {
-          console.error(`加载图片失败: ${url}`, error);
-        }
+  // 3D 拖拽处理
+  const handleMouseDown = useCallback((e: React.MouseEvent) => {
+    setIsDragging(true);
+    previousMousePosition.current = { x: e.clientX, y: e.clientY };
+  }, []);
+
+  const handleMouseMove = useCallback((e: React.MouseEvent) => {
+    if (!isDragging) return;
+    
+    const deltaX = e.clientX - previousMousePosition.current.x;
+    const deltaY = e.clientY - previousMousePosition.current.y;
+    
+    rotation.current.y += deltaX * 0.5;
+    rotation.current.x -= deltaY * 0.5;
+    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})`;
+    }
+    
+    previousMousePosition.current = { x: e.clientX, y: e.clientY };
+  }, [isDragging]);
+
+  const handleMouseUp = useCallback(() => {
+    setIsDragging(false);
+  }, []);
+
+  // 滚轮缩放
+  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));
+    if (wrapperRef.current) {
+      wrapperRef.current.style.transform = `rotateX(${rotation.current.x}deg) rotateY(${rotation.current.y}deg) scale(${scale.current})`;
+    }
+  }, []);
+
+  // 触摸支持
+  const touchStartDistance = useRef(0);
+  const initialScale = useRef(1);
+
+  const handleTouchStart = useCallback((e: React.TouchEvent) => {
+    if (e.touches.length === 1) {
+      setIsDragging(true);
+      previousMousePosition.current = { x: e.touches[0].clientX, y: e.touches[0].clientY };
+    } else if (e.touches.length === 2) {
+      touchStartDistance.current = Math.hypot(
+        e.touches[0].clientX - e.touches[1].clientX,
+        e.touches[0].clientY - e.touches[1].clientY
+      );
+      initialScale.current = scale.current;
+    }
+  }, []);
+
+  const handleTouchMove = useCallback((e: React.TouchEvent) => {
+    e.preventDefault();
+    if (e.touches.length === 1 && isDragging) {
+      const deltaX = e.touches[0].clientX - previousMousePosition.current.x;
+      const deltaY = e.touches[0].clientY - previousMousePosition.current.y;
+      
+      rotation.current.y += deltaX * 0.5;
+      rotation.current.x -= deltaY * 0.5;
+      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})`;
+      }
+      previousMousePosition.current = { x: e.touches[0].clientX, y: e.touches[0].clientY };
+    } else if (e.touches.length === 2) {
+      const currentDistance = Math.hypot(
+        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));
+      if (wrapperRef.current) {
+        wrapperRef.current.style.transform = `rotateX(${rotation.current.x}deg) rotateY(${rotation.current.y}deg) scale(${scale.current})`;
       }
     }
+  }, [isDragging]);
 
-    setCachedImages(prev => ({ ...prev, ...images }));
-  };
+  const handleTouchEnd = useCallback(() => {
+    setIsDragging(false);
+  }, []);
 
-  // 加载图片
-  const loadImage = (url: string): Promise<HTMLImageElement> => {
-    return new Promise((resolve, reject) => {
-      const img = new Image();
-      img.crossOrigin = 'Anonymous';
-      img.src = url;
-      img.onload = () => resolve(img);
-      img.onerror = reject;
-    });
+  // 获取槽位状态
+  const getSlotStatus = (slot: LockCabinetSlotVO) => {
+    if (slot.status === '1') return '异常';
+    if (slot.isOccupied === '1') return '已占用';
+    return '空闲';
   };
 
-  // 绘制图形
-  const drawCanvas = () => {
-    const canvas = canvasRef.current;
-    if (!canvas) return;
-
-    const ctx = canvas.getContext('2d');
-    if (!ctx) return;
+  // 获取槽位类型名称
+  const getSlotTypeName = (slot: LockCabinetSlotVO) => {
+    return slot.slotType === '0' ? '钥匙' : '挂锁';
+  };
 
-    // 清空画布
-    ctx.clearRect(0, 0, canvas.width, canvas.height);
+  useEffect(() => {
+    if (cabinetId) {
+      getCabinetInfo();
+      getData();
+    }
+  }, [cabinetId]);
 
-    // 按行分组
+  // 按行分组槽位数据(用于 3D 显示)
+  const groupedSlots = React.useMemo(() => {
     const grouped: Record<number, LockCabinetSlotVO[]> = {};
     slotData.forEach(slot => {
       const row = slot.row || 0;
       if (!grouped[row]) grouped[row] = [];
       grouped[row].push(slot);
     });
+    return grouped;
+  }, [slotData]);
+
+  // 获取钥匙槽位(前4个)
+  const keySlots = React.useMemo(() => {
+    return slotData.filter(s => s.slotType === '0').slice(0, 4);
+  }, [slotData]);
+
+  // 获取挂锁槽位(按行分组,最多5行,每行10个)
+  const lockSlots = React.useMemo(() => {
+    const locks = slotData.filter(s => s.slotType === '1');
+    const rows: LockCabinetSlotVO[][] = [];
+    for (let i = 0; i < 5; i++) {
+      rows.push(locks.slice(i * 10, (i + 1) * 10));
+    }
+    return rows;
+  }, [slotData]);
 
-    const rows = Object.keys(grouped).map(Number).sort((a, b) => a - b);
-    const startY = 20;
-    const rowHeight = 120;
-    const rowGap = 20;
-    const boxWidth = 860;
-    const centerX = canvas.width / 2;
-    const boxStartX = centerX - boxWidth / 2;
-
-    rows.forEach((rowKey, rowIndex) => {
-      const rowSlots = grouped[rowKey];
-
-      // 绘制行容器
-      ctx.strokeStyle = 'black';
-      ctx.lineWidth = 2;
-      ctx.strokeRect(
-        boxStartX,
-        startY + rowIndex * (rowHeight + rowGap),
-        boxWidth,
-        rowHeight
-      );
+  return (
+    <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%' }}
+          onMouseDown={handleMouseDown}
+          onMouseMove={handleMouseMove}
+          onMouseUp={handleMouseUp}
+          onMouseLeave={handleMouseUp}
+          onWheel={handleWheel}
+          onTouchStart={handleTouchStart}
+          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="absolute"
+                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',
+                }}
+              />
+
+              {/* 右侧面 */}
+              <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="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="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>
+                    <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>
+                            ))}
+                          </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="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>
+                      ))}
+                    </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>
+                        ))}
+                      </div>
+                    ))}
+                  </div>
+                </div>
+              </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>
 
-      // 绘制仓位
-      rowSlots.forEach((slot, slotIndex) => {
-        const { slotType, isOccupied, status } = slot;
-        let baseKey = '';
-
-        if (slotType === '0') {
-          baseKey = isOccupied === '1' ? 'icon.locker.normal' : 'icon.locker.out';
-        } else {
-          baseKey = isOccupied === '1' ? 'icon.padlock.normal' : 'icon.padlock.out';
-        }
-
-        const baseUrl = cachedResults[baseKey];
-        const baseImg = baseUrl ? cachedImages[baseUrl] : null;
-
-        const width = slotType === '0' ? 110 : 40;
-        const height = 90;
-        const padding = 20;
-        const totalSlots = rowSlots.length;
-        const spacing = totalSlots > 1
-          ? (boxWidth - 2 * padding - totalSlots * width) / (totalSlots - 1)
-          : 0;
-
-        const x = boxStartX + padding + slotIndex * (width + spacing);
-        const y = startY + rowIndex * (rowHeight + rowGap) + (rowHeight - height) / 2;
-
-        // 绘制仓位图标
-        if (baseImg) {
-          ctx.drawImage(baseImg, x, y, width, height);
-        } else {
-          // 如果没有图片,绘制占位矩形
-          ctx.fillStyle = '#f0f0f0';
-          ctx.fillRect(x, y, width, height);
-          ctx.strokeStyle = '#ccc';
-          ctx.lineWidth = 1;
-          ctx.strokeRect(x, y, width, height);
-        }
-
-        // 绘制异常图标
-        if (status === '1') {
-          const exUrl = cachedResults['icon.locker.exception'];
-          const exImg = exUrl ? cachedImages[exUrl] : null;
-          if (exImg) {
-            const exWidth = 30;
-            const exHeight = 30;
-            ctx.drawImage(
-              exImg,
-              x + (width - exWidth) / 2,
-              y + (height - exHeight) / 2,
-              exWidth,
-              exHeight
-            );
-          }
-        }
-
-        // 添加点击事件处理
-        canvas.addEventListener('click', (e) => {
-          const rect = canvas.getBoundingClientRect();
-          const clickX = e.clientX - rect.left;
-          const clickY = e.clientY - rect.top;
-
-          if (clickX >= x && clickX <= x + width && clickY >= y && clickY <= y + height) {
-            if (status === '1') {
-              setErrorInfo(slot.remark || '未知异常');
-              setDialogVisible(true);
-            } else {
-              console.log('点击仓位:', slot);
-            }
-          }
-        });
-      });
-    });
-  };
+      {/* 右侧:详情信息 */}
+      <div className="w-[420px] h-full min-h-0 bg-white rounded-lg shadow-sm p-5 overflow-hidden flex flex-col">
+        {/* 顶部信息(固定不滚动) */}
+        <div className="shrink-0">
+          {/* 标题 */}
+          <div className="mb-4">
+            <h2 className="text-lg font-bold text-gray-900 mb-2">锁控柜信息</h2>
+            <div className="h-1 bg-blue-500 rounded" />
+          </div>
 
-  useEffect(() => {
-    if (cabinetId) {
-      getData();
-    }
-  }, [cabinetId]);
+          {/* 基本信息 */}
+          <div className="mb-4 space-y-2">
+          <div className="flex justify-between items-center py-2 border-b border-gray-200">
+            <span className="text-gray-600">名称</span>
+            <span className="text-gray-900 font-medium">{cabinetInfo?.cabinetName || '-'}</span>
+          </div>
+          <div className="flex justify-between items-center py-2 border-b border-gray-200">
+            <span className="text-gray-600">硬件ID</span>
+            <span className="text-gray-900 font-medium">{cabinetInfo?.hardwareId || '-'}</span>
+          </div>
+          <div className="flex justify-between items-center py-2 border-b border-gray-200">
+            <span className="text-gray-600">序列号</span>
+            <span className="text-gray-900 font-medium">{cabinetInfo?.serialNumber || '-'}</span>
+          </div>
+          <div className="flex justify-between items-center py-2 border-b border-gray-200">
+            <span className="text-gray-600">岗位</span>
+            <span className="text-gray-900 font-medium">{cabinetInfo?.workstationName || '-'}</span>
+          </div>
+          <div className="flex justify-between items-center py-2 border-b border-gray-200">
+            <span className="text-gray-600">在线状态</span>
+            <span className={`px-3 py-1 rounded-full text-sm font-medium ${
+              cabinetInfo?.isOnline === '1' 
+                ? 'bg-green-100 text-green-700' 
+                : 'bg-gray-100 text-gray-700'
+            }`}>
+              {cabinetInfo?.isOnline === '1' ? '在线' : '离线'}
+            </span>
+          </div>
+          <div className="flex justify-between items-center py-2 border-b border-gray-200">
+            <span className="text-gray-600">运行状态</span>
+            <span className={`px-3 py-1 rounded-full text-sm font-medium ${
+              cabinetInfo?.status === '0' 
+                ? 'bg-blue-100 text-blue-700' 
+                : cabinetInfo?.status === '1'
+                ? 'bg-red-100 text-red-700'
+                : 'bg-gray-100 text-gray-700'
+            }`}>
+              {cabinetInfo?.status === '0' ? '正常' : cabinetInfo?.status === '1' ? '异常' : '未知'}
+            </span>
+          </div>
+          </div>
 
-  useEffect(() => {
-    if (slotData.length > 0 && Object.keys(cachedImages).length > 0) {
-      drawCanvas();
-    }
-  }, [slotData, cachedImages, cachedResults]);
+          {/* 统计卡片:左侧(总槽位/空闲),右侧两个盒子(上:已占用,下:异常,随机数) */}
+          <div className="grid grid-cols-2 gap-3 mb-3">
+            {/* 左列 */}
+            <div className="flex flex-col gap-3">
+              <div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl p-3 text-white shadow-lg">
+                <div className="text-2xl font-bold leading-none mb-1">{stats.total}</div>
+                <div className="text-xs opacity-90">总槽位</div>
+              </div>
+              <div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-xl p-3 text-white shadow-lg">
+                <div className="text-2xl font-bold leading-none mb-1">{stats.free}</div>
+                <div className="text-xs opacity-90">空闲</div>
+              </div>
+            </div>
+
+            {/* 右列(随机数) */}
+            <div className="flex flex-col gap-3">
+              <div
+                className="rounded-xl p-3 text-white shadow-lg"
+                style={{ background: 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)' }}
+              >
+                <div className="text-2xl font-bold leading-none mb-1">{randomRightStats.occupied}</div>
+                <div className="text-xs opacity-90">已占用</div>
+              </div>
+              <div
+                className="rounded-xl p-3 text-white shadow-lg"
+                style={{ background: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)' }}
+              >
+                <div className="text-2xl font-bold leading-none mb-1">{randomRightStats.abnormal}</div>
+                <div className="text-xs opacity-90">异常</div>
+              </div>
+            </div>
+          </div>
+        </div>
 
-  return (
-    <div className="space-y-4">
-      {/* 时间卡片 */}
-      {updateTime && (
-        <div className="inline-flex items-center gap-3 px-5 py-3 bg-gradient-to-r from-gray-50 to-gray-100 rounded-lg shadow-sm">
-          <Clock className="w-5 h-5 text-blue-500" />
+        {/* 列表区域(超出才滚动) */}
+        <div className="flex-1 min-h-0 overflow-y-auto pr-1">
+          {/* 设备列表 */}
           <div>
-            <div className="text-xs text-gray-500">最后更新</div>
-            <div className="text-base font-bold text-gray-800">{updateTime}</div>
+            <h3 className="text-base font-bold text-gray-900 mb-3">设备列表</h3>
+            <div className="space-y-3">
+              {slotData.slice(0, 10).map((slot, idx) => (
+                <div
+                  key={idx}
+                  className="bg-gray-50 rounded-lg p-3 border border-gray-200 hover:shadow-md transition-shadow"
+                >
+                  <div className="flex items-center justify-between mb-2">
+                    <span className="font-medium text-gray-900">
+                      {getSlotTypeName(slot)}-{slot.slotName || slot.slotCode || idx + 1}
+                    </span>
+                    <span className={`px-2 py-1 rounded-full text-xs font-medium ${
+                      slot.status === '1'
+                        ? 'bg-red-100 text-red-700'
+                        : slot.isOccupied === '1'
+                        ? 'bg-green-100 text-green-700'
+                        : 'bg-gray-100 text-gray-700'
+                    }`}>
+                      {getSlotStatus(slot)}
+                    </span>
+                  </div>
+                  <div className="text-sm text-gray-600">
+                    {getSlotTypeName(slot)} · 槽位 {slot.slotName || slot.slotCode || idx + 1} ·
+                  </div>
+                </div>
+              ))}
+            </div>
           </div>
+
+          {/* 更新时间 */}
+          {updateTime && (
+            <div className="mt-4 inline-flex items-center gap-3 px-4 py-2 bg-gradient-to-r from-gray-50 to-gray-100 rounded-lg">
+              <Clock className="w-4 h-4 text-blue-500" />
+              <div>
+                <div className="text-xs text-gray-500">最后更新</div>
+                <div className="text-sm font-bold text-gray-800">{updateTime}</div>
+              </div>
+            </div>
+          )}
         </div>
-      )}
-
-      {/* 画布容器 */}
-      <div className="bg-white rounded-lg shadow-sm p-4 overflow-auto">
-        <canvas
-          ref={canvasRef}
-          width={900}
-          height={1000}
-          className="border border-gray-200"
-        />
       </div>
 
       {/* 异常信息对话框 */}
@@ -264,4 +676,3 @@ export default function MapData({ cabinetId }: MapDataProps) {
     </div>
   );
 }
-

+ 8 - 8
src/components/user/UserAssignRoleForm.tsx

@@ -40,7 +40,7 @@ const UserAssignRoleForm = forwardRef<UserAssignRoleFormRef, UserAssignRoleFormP
       setRoleList(roles || []);
     } catch (error) {
       console.error('加载角色列表失败:', error);
-      message.error(t('form.loadRoleListFailed'));
+      message.error(t('common.loadRoleListFailed'));
     }
 
     // 获取用户角色列表
@@ -52,7 +52,7 @@ const UserAssignRoleForm = forwardRef<UserAssignRoleFormRef, UserAssignRoleFormP
       });
     } catch (error: any) {
       console.error('获取用户角色失败:', error);
-      message.error(error.message || t('form.getUserRoleFailed'));
+      message.error(error.message || t('common.getUserRoleFailed'));
     } finally {
       setFormLoading(false);
     }
@@ -75,11 +75,11 @@ const UserAssignRoleForm = forwardRef<UserAssignRoleFormRef, UserAssignRoleFormP
         };
         console.log('分配角色参数:', params);
         await userPermissionApi.assignUserRole(params);
-        message.success(t('form.assignSuccess'));
+        message.success(t('common.assignSuccess'));
         setDialogVisible(false);
         onSuccess?.();
       } catch (error: any) {
-        message.error(error.message || t('form.assignFailed'));
+        message.error(error.message || t('common.assignFailed'));
       } finally {
         setFormLoading(false);
       }
@@ -90,7 +90,7 @@ const UserAssignRoleForm = forwardRef<UserAssignRoleFormRef, UserAssignRoleFormP
 
   return (
     <Modal
-      title={t('form.assignRoleTitle')}
+      title={t('common.assignRoleTitle')}
       open={dialogVisible}
       onCancel={() => setDialogVisible(false)}
       onOk={submitForm}
@@ -106,18 +106,18 @@ const UserAssignRoleForm = forwardRef<UserAssignRoleFormRef, UserAssignRoleFormP
           roleIds: [],
         }}
       >
-        <Form.Item label={t('form.userName')} name="username">
+        <Form.Item label={t('common.userName')} name="username">
           <Input disabled />
         </Form.Item>
 
-        <Form.Item label={t('form.nickname')} name="nickname">
+        <Form.Item label={t('common.nickname')} name="nickname">
           <Input disabled />
         </Form.Item>
 
         <Form.Item label={t('common.assignRole')} name="roleIds">
           <Select
             mode="multiple"
-            placeholder={t('form.selectRole')}
+            placeholder={t('common.selectRole')}
             options={roleList.map(role => ({
               label: role.name,
               value: role.id,

+ 2 - 1
src/routes/index.tsx

@@ -6,6 +6,7 @@ import ProtectedRoute from '../components/ProtectedRoute';
 import ProcessDesigner from '../components/ProcessDesigner';
 import FormDesigner from '../components/FormDesigner';
 import WorkJobDetail from '../components/WorkJobDetail';
+import LockCabinetDetail from '../components/lockCabinet/LockCabinetDetail';
 
 // 路由配置
 export const router = createBrowserRouter([
@@ -35,7 +36,7 @@ export const router = createBrowserRouter([
     path: '/lock-cabinet/detail',
     element: (
       <ProtectedRoute>
-        <Dashboard />
+        <LockCabinetDetail />
       </ProtectedRoute>
     ),
   },