pm пре 3 месеци
родитељ
комит
1d44d800c0
2 измењених фајлова са 961 додато и 293 уклоњено
  1. 756 0
      src/components/lockCabinet/LockCabinet3D.css
  2. 205 293
      src/components/lockCabinet/MapData.tsx

+ 756 - 0
src/components/lockCabinet/LockCabinet3D.css

@@ -0,0 +1,756 @@
+/* 该文件为 3D 锁控柜的 1:1 复刻样式(基于用户提供的 HTML/CSS)
+   为避免全局污染,统一加了 lc3d 前缀命名空间。 */
+
+.lc3dRoot {
+  --scene-width: 800px;
+  --scene-height: 1100px;
+
+  --cabinet-width: 480px;
+  --cabinet-height: 900px;
+  --cabinet-depth: 200px;
+}
+
+*,
+*::before,
+*::after {
+  box-sizing: border-box;
+}
+
+/* 3D 场景容器 */
+.lc3dScene {
+  width: var(--scene-width);
+  height: var(--scene-height);
+  perspective: 2000px;
+  perspective-origin: 50% 50%;
+  position: relative;
+}
+
+/* 3D 柜体包装器 */
+.lc3dCabinetWrapper {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  transform-style: preserve-3d;
+  transition: transform 0.1s ease-out;
+  will-change: transform;
+}
+
+/* 3D 盒子容器 */
+.lc3dCabinet3d {
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  width: var(--cabinet-width);
+  height: var(--cabinet-height);
+  transform-style: preserve-3d;
+  transform: translateX(-50%) translateY(-50%);
+}
+
+/* ========== 正面 ========== */
+.lc3dCabinetFront {
+  width: var(--cabinet-width);
+  height: var(--cabinet-height);
+  background: linear-gradient(180deg, #d8d8d8 0%, #c8c8c8 50%, #b8b8b8 100%);
+  border-radius: 8px;
+  position: absolute;
+  left: 0;
+  top: 0;
+  transform: translateZ(0);
+  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5), inset 0 -1px 0 rgba(0, 0, 0, 0.2);
+  border: 3px solid #a0a0a0;
+}
+
+/* 顶部边框装饰 */
+.lc3dCabinetFront::before {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  height: 60px;
+  background: linear-gradient(180deg, #e0e0e0 0%, #d0d0d0 100%);
+  border-radius: 5px 5px 0 0;
+}
+
+/* 环境光效果 */
+.lc3dCabinetFront::after {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: linear-gradient(
+    135deg,
+    rgba(255, 255, 255, 0.08) 0%,
+    transparent 50%,
+    rgba(0, 0, 0, 0.05) 100%
+  );
+  pointer-events: none;
+  border-radius: 8px;
+}
+
+/* ========== 背面 ========== */
+.lc3dCabinetBack {
+  width: var(--cabinet-width);
+  height: var(--cabinet-height);
+  background: linear-gradient(180deg, #808080 0%, #707070 50%, #606060 100%);
+  position: absolute;
+  left: 0;
+  top: 0;
+  transform: translateZ(calc(var(--cabinet-depth) * -1));
+  border-radius: 8px;
+  border: 3px solid #555;
+}
+
+/* 背面内部细节 */
+.lc3dCabinetBack::before {
+  content: '';
+  position: absolute;
+  top: 20px;
+  left: 20px;
+  right: 20px;
+  bottom: 20px;
+  background: linear-gradient(180deg, #757575 0%, #656565 100%);
+  border-radius: 4px;
+  border: 2px solid #555;
+}
+
+/* 背面通风孔 */
+.lc3dCabinetBack::after {
+  content: '';
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  width: 200px;
+  height: 400px;
+  background: repeating-linear-gradient(0deg, #444 0px, #444 3px, #666 3px, #666 8px);
+  border-radius: 4px;
+  opacity: 0.8;
+}
+
+/* ========== 左侧面 ========== */
+.lc3dCabinetLeft {
+  width: var(--cabinet-depth);
+  height: var(--cabinet-height);
+  background: linear-gradient(90deg, #989898 0%, #a8a8a8 30%, #b8b8b8 70%, #c8c8c8 100%);
+  position: absolute;
+  left: calc(var(--cabinet-depth) * -1);
+  top: 0;
+  transform-origin: right center;
+  transform: rotateY(-90deg);
+  border-top: 3px solid #b0b0b0;
+  border-bottom: 3px solid #808080;
+  border-left: 3px solid #888;
+}
+
+/* 左侧面内部装饰 */
+.lc3dCabinetLeft::before {
+  content: '';
+  position: absolute;
+  top: 30px;
+  left: 15px;
+  right: 15px;
+  bottom: 30px;
+  background: linear-gradient(90deg, #909090 0%, #a0a0a0 50%, #b0b0b0 100%);
+  border-radius: 3px;
+  border: 1px solid #888;
+}
+
+/* ========== 右侧面 ========== */
+.lc3dCabinetRight {
+  width: var(--cabinet-depth);
+  height: var(--cabinet-height);
+  background: linear-gradient(270deg, #888888 0%, #989898 30%, #a8a8a8 70%, #b0b0b0 100%);
+  position: absolute;
+  left: var(--cabinet-width);
+  top: 0;
+  transform-origin: left center;
+  transform: rotateY(90deg);
+  border-top: 3px solid #a0a0a0;
+  border-bottom: 3px solid #777;
+  border-right: 3px solid #777;
+}
+
+/* 右侧面内部装饰 */
+.lc3dCabinetRight::before {
+  content: '';
+  position: absolute;
+  top: 30px;
+  left: 15px;
+  right: 15px;
+  bottom: 30px;
+  background: linear-gradient(270deg, #808080 0%, #909090 50%, #a0a0a0 100%);
+  border-radius: 3px;
+  border: 1px solid #777;
+}
+
+/* ========== 顶部 ========== */
+.lc3dCabinetTop {
+  width: var(--cabinet-width);
+  height: var(--cabinet-depth);
+  background: linear-gradient(180deg, #d0d0d0 0%, #c8c8c8 30%, #c0c0c0 70%, #b8b8b8 100%);
+  position: absolute;
+  left: 0;
+  top: calc(var(--cabinet-depth) * -1);
+  transform-origin: center bottom;
+  transform: rotateX(90deg);
+  border-left: 3px solid #b8b8b8;
+  border-right: 3px solid #a8a8a8;
+  border-top: 3px solid #909090;
+}
+
+/* 顶部装饰 */
+.lc3dCabinetTop::before {
+  content: '';
+  position: absolute;
+  top: 15px;
+  left: 30px;
+  right: 30px;
+  bottom: 15px;
+  background: linear-gradient(180deg, #c8c8c8 0%, #b8b8b8 100%);
+  border-radius: 3px;
+  border: 1px solid #a0a0a0;
+}
+
+/* ========== 底部 ========== */
+.lc3dCabinetBottom {
+  width: var(--cabinet-width);
+  height: var(--cabinet-depth);
+  background: linear-gradient(0deg, #606060 0%, #707070 30%, #787878 70%, #808080 100%);
+  position: absolute;
+  left: 0;
+  top: var(--cabinet-height);
+  transform-origin: center top;
+  transform: rotateX(-90deg);
+  border-left: 3px solid #707070;
+  border-right: 3px solid #606060;
+  border-bottom: 3px solid #505050;
+}
+
+/* 底部装饰 */
+.lc3dCabinetBottom::before {
+  content: '';
+  position: absolute;
+  top: 15px;
+  left: 30px;
+  right: 30px;
+  bottom: 15px;
+  background: linear-gradient(0deg, #585858 0%, #686868 100%);
+  border-radius: 3px;
+  border: 1px solid #555;
+}
+
+/* 地面阴影 */
+.lc3dGroundShadow {
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  transform: translateX(-50%) translateY(calc(450px + 80px)) rotateX(90deg);
+  width: 650px;
+  height: 300px;
+  background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0.2) 0%, transparent 65%);
+  filter: blur(20px);
+  pointer-events: none;
+}
+
+/* 挂钩 */
+.lc3dHook {
+  position: absolute;
+  top: -15px;
+  width: 30px;
+  height: 25px;
+  background: linear-gradient(180deg, #999 0%, #666 100%);
+  border-radius: 50% 50% 0 0;
+  transform: translateZ(5px);
+}
+
+.lc3dHookLeft {
+  left: 30px;
+}
+
+.lc3dHookRight {
+  right: 30px;
+}
+
+.lc3dHook::after {
+  content: '';
+  position: absolute;
+  top: 10px;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 12px;
+  height: 12px;
+  background: radial-gradient(circle at 30% 30%, #666, #333);
+  border-radius: 50%;
+}
+
+/* 头部区域 */
+.lc3dHeader {
+  position: relative;
+  z-index: 1;
+  padding: 15px 20px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.lc3dLogo {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.lc3dLogoIcon {
+  width: 50px;
+  height: 40px;
+  position: relative;
+}
+
+.lc3dLogoIcon svg {
+  width: 100%;
+  height: 100%;
+  filter: drop-shadow(2px 2px 2px rgba(0, 0, 0, 0.3));
+}
+
+.lc3dLogoText {
+  color: #c41e3a;
+  font-size: 24px;
+  font-weight: bold;
+  letter-spacing: 2px;
+  text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
+}
+
+.lc3dTitle {
+  color: #c41e3a;
+  font-size: 22px;
+  font-weight: bold;
+  text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
+}
+
+/* 主面板 */
+.lc3dMainPanel {
+  margin: 10px 15px;
+  background: linear-gradient(180deg, #e8e8e8 0%, #d0d0d0 100%);
+  border-radius: 4px;
+  padding: 15px;
+  box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.2);
+  transform: translateZ(5px);
+}
+
+/* 屏幕区域 */
+.lc3dScreenSection {
+  display: flex;
+  justify-content: center;
+  margin-bottom: 15px;
+}
+
+.lc3dMonitorFrame {
+  background: linear-gradient(180deg, #2a2a2a 0%, #1a1a1a 50%, #0a0a0a 100%);
+  border-radius: 8px;
+  padding: 15px 20px 45px 20px;
+  position: relative;
+  box-shadow: 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;
+}
+
+.lc3dScreen {
+  width: 320px;
+  height: 180px;
+  background: #1a3a5c;
+  border-radius: 4px;
+  border: 3px solid #444;
+  overflow: hidden;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4), inset 0 0 30px rgba(0, 100, 200, 0.3);
+}
+
+.lc3dScreenContent {
+  width: 100%;
+  height: 100%;
+  background: linear-gradient(180deg, #0d4a7a 0%, #0a3d66 100%);
+  padding: 10px;
+}
+
+.lc3dScreenHeader {
+  display: flex;
+  justify-content: space-between;
+  color: #fff;
+  font-size: 10px;
+  margin-bottom: 8px;
+}
+
+.lc3dScreenTable {
+  background: rgba(255, 255, 255, 0.1);
+  border-radius: 2px;
+  padding: 5px;
+}
+
+.lc3dScreenRow {
+  display: flex;
+  justify-content: space-between;
+  color: #00ffff;
+  font-size: 9px;
+  padding: 3px 5px;
+  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.lc3dScreenRow:last-child {
+  border-bottom: none;
+}
+
+.lc3dScreenButtons {
+  display: flex;
+  gap: 10px;
+  margin-top: 10px;
+  justify-content: center;
+}
+
+.lc3dScreenBtn {
+  padding: 4px 15px;
+  font-size: 10px;
+  border-radius: 3px;
+  border: none;
+  cursor: pointer;
+}
+
+.lc3dScreenBtnRed {
+  background: linear-gradient(180deg, #ff5555, #cc0000);
+  color: white;
+  box-shadow: 0 2px 4px rgba(255, 0, 0, 0.4);
+}
+
+.lc3dScreenBtnGreen {
+  background: linear-gradient(180deg, #55cc55, #009900);
+  color: white;
+  box-shadow: 0 2px 4px rgba(0, 255, 0, 0.4);
+}
+
+.lc3dMonitorBottomArea {
+  position: absolute;
+  bottom: 8px;
+  right: 12px;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.lc3dCardReader {
+  width: 50px;
+  height: 28px;
+  background: linear-gradient(180deg, #3a3a3a 0%, #252525 100%);
+  border-radius: 3px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border: 1px solid #444;
+  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.4);
+}
+
+.lc3dCardReaderIcon {
+  width: 20px;
+  height: 16px;
+  border: 1px solid #555;
+  border-radius: 2px;
+  background: linear-gradient(180deg, #444, #333);
+  position: relative;
+}
+
+.lc3dCardReaderIcon::after {
+  content: '';
+  position: absolute;
+  top: 3px;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 10px;
+  height: 6px;
+  background: #555;
+  border-radius: 1px;
+}
+
+.lc3dFingerprintReader {
+  width: 28px;
+  height: 28px;
+  background: linear-gradient(180deg, #3a3a3a 0%, #252525 100%);
+  border-radius: 4px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border: 1px solid #444;
+  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.4);
+}
+
+.lc3dFingerprintIcon {
+  width: 18px;
+  height: 18px;
+  border: 2px solid #555;
+  border-radius: 50%;
+  position: relative;
+}
+
+.lc3dFingerprintIcon::before {
+  content: '';
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  width: 10px;
+  height: 10px;
+  border: 1px solid #555;
+  border-radius: 50%;
+}
+
+.lc3dFingerprintIcon::after {
+  content: '';
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  width: 4px;
+  height: 4px;
+  background: #555;
+  border-radius: 50%;
+}
+
+/* 抽屉 */
+.lc3dDrawer {
+  background: linear-gradient(180deg, #f8f8f8 0%, #e0e0e0 100%);
+  height: 35px;
+  margin: 10px 0;
+  border-radius: 3px;
+  border: 1px solid #bbb;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.8);
+  transform: translateZ(12px);
+}
+
+.lc3dDrawerHandle {
+  width: 40px;
+  height: 8px;
+  background: linear-gradient(180deg, #bbb, #888);
+  border-radius: 4px;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
+}
+
+/* ========== 钥匙仓区域 ========== */
+.lc3dKeySection {
+  padding: 15px 0;
+  border-bottom: 2px solid #aaa;
+  transform: translateZ(5px);
+}
+
+.lc3dKeySectionTitle {
+  text-align: center;
+  font-size: 12px;
+  color: #666;
+  margin-bottom: 10px;
+  font-weight: bold;
+}
+
+.lc3dKeyRow {
+  display: flex;
+  justify-content: center;
+  gap: 20px;
+}
+
+.lc3dKeySlot {
+  width: 50px;
+  height: 50px;
+  background: linear-gradient(180deg, #3a3a3a 0%, #1a1a1a 100%);
+  border-radius: 50%;
+  border: 3px solid #555;
+  position: relative;
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4), inset 0 2px 4px rgba(0, 0, 0, 0.5);
+  transform: translateZ(10px);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.lc3dKeySlot::before {
+  content: '';
+  width: 30px;
+  height: 30px;
+  background: linear-gradient(180deg, #2a2a2a 0%, #0a0a0a 100%);
+  border-radius: 50%;
+  border: 2px solid #444;
+}
+
+.lc3dKeyNumber {
+  position: absolute;
+  top: -20px;
+  left: 50%;
+  transform: translateX(-50%);
+  font-size: 12px;
+  font-weight: bold;
+  color: #555;
+  text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5);
+}
+
+.lc3dKeyIndicator {
+  position: absolute;
+  bottom: -8px;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+}
+
+.lc3dKeyIndicatorRed {
+  background: radial-gradient(circle at 30% 30%, #ff6666, #aa0000);
+  box-shadow: 0 0 6px #ff0000;
+}
+
+.lc3dKeyIndicatorGreen {
+  background: radial-gradient(circle at 30% 30%, #66ff66, #00aa00);
+  box-shadow: 0 0 6px #00ff00;
+}
+
+/* ========== 锁仓区域 ========== */
+.lc3dLockSection {
+  padding: 10px 0;
+  transform: translateZ(3px);
+}
+
+.lc3dLockSectionTitle {
+  text-align: center;
+  font-size: 12px;
+  color: #666;
+  margin-bottom: 8px;
+  margin-top: 10px;
+  font-weight: bold;
+}
+
+.lc3dLockRow {
+  display: flex;
+  justify-content: center;
+  gap: 6px;
+  margin-bottom: 8px;
+  margin-top: 18px;
+}
+
+.lc3dLockRow:first-child {
+  margin-top: 20px;
+}
+
+.lc3dLockSlot {
+  width: 38px;
+  height: 45px;
+  background: linear-gradient(180deg, #4a4a4a 0%, #2a2a2a 100%);
+  border-radius: 3px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 4px;
+  border: 1px solid #555;
+  box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.4), 0 2px 4px rgba(0, 0, 0, 0.3);
+  transform: translateZ(8px);
+  transition: transform 0.2s;
+  position: relative;
+}
+
+.lc3dLockSlot:hover {
+  transform: translateZ(15px);
+}
+
+.lc3dLockNumber {
+  position: absolute;
+  top: -16px;
+  left: 50%;
+  transform: translateX(-50%);
+  font-size: 10px;
+  font-weight: bold;
+  color: #555;
+  text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5);
+}
+
+.lc3dLockIndicator {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  margin-bottom: 3px;
+}
+
+.lc3dLockIndicatorRed {
+  background: radial-gradient(circle at 30% 30%, #ff6666, #aa0000);
+  box-shadow: 0 0 6px #ff0000;
+}
+
+.lc3dLockIndicatorOff {
+  background: radial-gradient(circle at 30% 30%, #555, #333);
+}
+
+.lc3dLockHole {
+  width: 20px;
+  height: 25px;
+  background: linear-gradient(180deg, #1a1a1a 0%, #050505 100%);
+  border-radius: 2px;
+  border: 1px solid #444;
+  box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.5);
+}
+
+/* 边框螺丝装饰 */
+.lc3dScrew {
+  position: absolute;
+  width: 10px;
+  height: 10px;
+  background: radial-gradient(circle at 30% 30%, #999, #444);
+  border-radius: 50%;
+  border: 1px solid #333;
+  box-shadow: 0 2px 3px rgba(0, 0, 0, 0.3);
+  transform: translateZ(5px);
+}
+
+.lc3dScrew::after {
+  content: '+';
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  color: #333;
+  font-size: 8px;
+  font-weight: bold;
+}
+
+.lc3dScrewsLeft {
+  position: absolute;
+  left: 8px;
+  top: 80px;
+  display: flex;
+  flex-direction: column;
+  gap: 50px;
+  z-index: 1;
+}
+
+.lc3dScrewsRight {
+  position: absolute;
+  right: 8px;
+  top: 80px;
+  display: flex;
+  flex-direction: column;
+  gap: 50px;
+  z-index: 1;
+}
+
+/* 提示 */
+.lc3dControlsHint {
+  position: absolute;
+  bottom: 12px;
+  left: 50%;
+  transform: translateX(-50%);
+  color: rgba(0, 0, 0, 0.5);
+  font-size: 12px;
+  text-align: center;
+  pointer-events: none;
+  background: rgba(255, 255, 255, 0.7);
+  border: 1px solid rgba(229, 231, 235, 1);
+  padding: 6px 10px;
+  border-radius: 9999px;
+  backdrop-filter: blur(10px);
+}
+

+ 205 - 293
src/components/lockCabinet/MapData.tsx

@@ -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>
 
       {/* 右侧:详情信息 */}