Przeglądaj źródła

组态分辨率适配调整,设备开关按钮,管道调整

Zimo 1 dzień temu
rodzic
commit
4ac534d42c
2 zmienionych plików z 573 dodań i 385 usunięć
  1. 142 171
      src/views/test/air.vue
  2. 431 214
      src/views/test/index.vue

+ 142 - 171
src/views/test/air.vue

@@ -20,7 +20,7 @@ const initGraph = () => {
 
   graph.value = new Graph({
     container: psacontainerRef.value,
-    interacting: false,
+    interacting: true,
     autoResize: true,
     panning: true,
     mousewheel: { enabled: true, zoomAtMousePosition: true, modifiers: ['ctrl', 'meta'] },
@@ -273,19 +273,19 @@ const edgeAttrs = {
     fill: {
       connection: true,
       strokeWidth: 10,
-      stroke: '#0F5BB5',
+      stroke: '#fcfeff',
       strokeLinecap: 'round',
       fill: 'none'
     },
     line: {
       connection: true,
       strokeWidth: 4,
-      stroke: '#fff',
+      stroke: '#0F5BB5',
       strokeDasharray: '20, 30',
       strokeLinecap: 'round',
       targetMarker: null,
       fill: 'none',
-      filter: 'drop-shadow(0 0 3px #fff)',
+      filter: 'drop-shadow(0 0 3px #0F5BB5)',
       style: {
         animation: 'edge-dash-flow 1.2s infinite linear'
       }
@@ -302,19 +302,19 @@ const edgeAttrs2 = {
     fill: {
       connection: true,
       strokeWidth: 10,
-      stroke: '#2bbbc6',
+      stroke: '#fcfeff',
       strokeLinecap: 'round',
       fill: 'none'
     },
     line: {
       connection: true,
       strokeWidth: 4,
-      stroke: '#fff',
+      stroke: '#2bbbc6',
       strokeDasharray: '20, 30',
       strokeLinecap: 'round',
       targetMarker: null,
       fill: 'none',
-      filter: 'drop-shadow(0 0 3px #fff)',
+      filter: 'drop-shadow(0 0 3px #2bbbc6)',
       style: {
         animation: 'edge-dash-flow 1.2s infinite linear'
       }
@@ -322,6 +322,38 @@ const edgeAttrs2 = {
   }
 }
 
+function gendertargetMarker(
+  type: 'blue' | 'green',
+  text: string,
+  distance: number,
+  offset: number
+) {
+  return {
+    targetMarker: {
+      name: 'block',
+      width: 18,
+      height: 24,
+      fill: type === 'blue' ? '#0F5BB5' : '#2bbbc6',
+      stroke: 'none',
+      offset: 5
+    },
+    labels: [
+      {
+        attrs: {
+          text: {
+            text: text,
+            fill: type === 'blue' ? '#00E5FF' : '#2bbbc6',
+            fontSize: 20,
+            fontWeight: 'bold'
+          },
+          rect: { fill: 'transparent' }
+        },
+        position: { distance: distance, offset: offset }
+      }
+    ]
+  }
+}
+
 const edgeDatas = reactive<any>([
   {
     id: 'ac-edge',
@@ -423,79 +455,34 @@ const edgeDatas = reactive<any>([
     source: { x: 1790, y: 498 },
     vertices: [{ x: 1790, y: 298 }],
     target: { x: 1960, y: 298 },
-    targetMarker: { name: 'block', width: 18, height: 24, fill: '#fff', stroke: 'none' },
-    labels: [
-      {
-        attrs: {
-          text: { text: 'PSA入口', fill: '#00E5FF', fontSize: 20, fontWeight: 'bold' },
-          rect: { fill: 'transparent' }
-        },
-        position: { distance: 0.78, offset: 25 }
-      }
-    ]
+    ...gendertargetMarker('blue', 'PSA入口', 0.78, 25)
   },
   {
     id: 'nit-edge-input2',
     source: { x: 1790, y: 498 },
     vertices: [{ x: 1790, y: 398 }],
     target: { x: 1960, y: 398 },
-    targetMarker: { name: 'block', width: 18, height: 24, fill: '#fff', stroke: 'none' },
-    labels: [
-      {
-        attrs: {
-          text: { text: 'PSA入口', fill: '#00E5FF', fontSize: 20, fontWeight: 'bold' },
-          rect: { fill: 'transparent' }
-        },
-        position: { distance: 0.7, offset: 25 }
-      }
-    ]
+    ...gendertargetMarker('blue', 'PSA入口', 0.7, 25)
   },
   {
     id: 'nit-edge-input3',
     source: { x: 1790, y: 498 },
     vertices: [{ x: 1790, y: 598 }],
     target: { x: 1960, y: 598 },
-    targetMarker: { name: 'block', width: 18, height: 24, fill: '#fff', stroke: 'none' },
-    labels: [
-      {
-        attrs: {
-          text: { text: 'PSA入口', fill: '#00E5FF', fontSize: 20, fontWeight: 'bold' },
-          rect: { fill: 'transparent' }
-        },
-        position: { distance: 0.7, offset: 25 }
-      }
-    ]
+    ...gendertargetMarker('blue', 'PSA入口', 0.7, 25)
   },
   {
     id: 'nit-edge-input4',
     source: { x: 1790, y: 498 },
     vertices: [{ x: 1790, y: 698 }],
     target: { x: 1960, y: 698 },
-    targetMarker: { name: 'block', width: 18, height: 24, fill: '#fff', stroke: 'none' },
-    labels: [
-      {
-        attrs: {
-          text: { text: 'PSA入口', fill: '#00E5FF', fontSize: 20, fontWeight: 'bold' },
-          rect: { fill: 'transparent' }
-        },
-        position: { distance: 0.78, offset: 25 }
-      }
-    ]
+    ...gendertargetMarker('blue', 'PSA入口', 0.78, 25)
   },
   {
     id: 'meter2-edge-out',
     source: { x: 1710, y: 498 },
     target: { x: 1960, y: 498 },
-    targetMarker: { name: 'block', width: 18, height: 24, fill: '#fff', stroke: 'none' },
-    labels: [
-      {
-        attrs: {
-          text: { text: 'PSA入口', fill: '#00E5FF', fontSize: 20, fontWeight: 'bold' },
-          rect: { fill: 'transparent' }
-        },
-        position: { distance: 0.68, offset: 25 }
-      }
-    ]
+    ...gendertargetMarker('blue', 'PSA入口', 0.68, 25)
   },
   {
     id: 'out',
@@ -506,91 +493,37 @@ const edgeDatas = reactive<any>([
       { x: 1000, y: 910 }
     ],
     target: { x: 1960, y: 910 },
-    targetMarker: { name: 'block', width: 18, height: 24, fill: '#fff', stroke: 'none' },
-    labels: [
-      {
-        attrs: {
-          text: { text: '排污收集箱', fill: '#2bbbc6', fontSize: 20, fontWeight: 'bold' },
-          rect: { fill: 'transparent' }
-        },
-        position: { distance: 0.95, offset: -25 }
-      }
-    ]
+    ...gendertargetMarker('green', '排污收集箱', 0.98, -25)
   },
   {
     id: 'ac-edge-input1',
     source: { x: 100, y: 1010 },
-    target: { x: 100, y: 790 },
-    targetMarker: { name: 'block', width: 18, height: 24, fill: '#fff', stroke: 'none' },
-    labels: [
-      {
-        attrs: {
-          text: { text: '空\n压\n机\n#1', fill: '#00E5FF', fontSize: 20, fontWeight: 'bold' },
-          rect: { fill: 'transparent' }
-        },
-        position: { distance: 0.5, offset: -25 }
-      }
-    ]
+    target: { x: 100, y: 797 },
+    ...gendertargetMarker('blue', '空\n压\n机\n#1', 0.5, -25)
   },
   {
     id: 'ac-edge-input2',
     source: { x: 200, y: 1010 },
-    target: { x: 200, y: 790 },
-    targetMarker: { name: 'block', width: 18, height: 24, fill: '#fff', stroke: 'none' },
-    labels: [
-      {
-        attrs: {
-          text: { text: '空\n压\n机\n2#', fill: '#00E5FF', fontSize: 20, fontWeight: 'bold' },
-          rect: { fill: 'transparent' }
-        },
-        position: { distance: 0.5, offset: -25 }
-      }
-    ]
+    target: { x: 200, y: 797 },
+    ...gendertargetMarker('blue', '空\n压\n机\n#2', 0.5, -25)
   },
   {
     id: 'ac-edge-input3',
     source: { x: 300, y: 1010 },
-    target: { x: 300, y: 790 },
-    targetMarker: { name: 'block', width: 18, height: 24, fill: '#fff', stroke: 'none' },
-    labels: [
-      {
-        attrs: {
-          text: { text: '空\n压\n机\n3#', fill: '#00E5FF', fontSize: 20, fontWeight: 'bold' },
-          rect: { fill: 'transparent' }
-        },
-        position: { distance: 0.5, offset: -25 }
-      }
-    ]
+    target: { x: 300, y: 797 },
+    ...gendertargetMarker('blue', '空\n压\n机\n#3', 0.5, -25)
   },
   {
     id: 'ac-edge-input4',
     source: { x: 400, y: 1010 },
-    target: { x: 400, y: 790 },
-    targetMarker: { name: 'block', width: 18, height: 24, fill: '#fff', stroke: 'none' },
-    labels: [
-      {
-        attrs: {
-          text: { text: '空\n压\n机\n4#', fill: '#00E5FF', fontSize: 20, fontWeight: 'bold' },
-          rect: { fill: 'transparent' }
-        },
-        position: { distance: 0.5, offset: -25 }
-      }
-    ]
+    target: { x: 400, y: 797 },
+    ...gendertargetMarker('blue', '空\n压\n机\n#4', 0.5, -25)
   },
   {
     id: 'ac-edge-input5',
     source: { x: 500, y: 1010 },
-    target: { x: 500, y: 790 },
-    targetMarker: { name: 'block', width: 18, height: 24, fill: '#fff', stroke: 'none' },
-    labels: [
-      {
-        attrs: {
-          text: { text: '空\n压\n机\n5#', fill: '#00E5FF', fontSize: 20, fontWeight: 'bold' },
-          rect: { fill: 'transparent' }
-        },
-        position: { distance: 0.5, offset: -25 }
-      }
-    ]
+    target: { x: 500, y: 797 },
+    ...gendertargetMarker('blue', '空\n压\n机\n#5', 0.5, -25)
   }
 ])
 
@@ -605,6 +538,27 @@ const renderEdges = () => {
   })
 }
 
+// 新增外层容器和目标缩放区域 Ref
+const wrapperRef = ref<HTMLDivElement>()
+const targetArea = ref<HTMLDivElement>()
+
+const updateScale = () => {
+  if (!wrapperRef.value || !targetArea.value) return
+
+  const currentWidth = wrapperRef.value.clientWidth
+  const currentHeight = wrapperRef.value.clientHeight
+
+  // 针对你的节点坐标设定一个刚好能包住的虚拟画布大小
+  const designWidth = 2200
+  const designHeight = 1200
+
+  const scaleX = currentWidth / designWidth
+  const scaleY = currentHeight / designHeight
+  const scale = Math.min(scaleX, scaleY)
+
+  targetArea.value.style.transform = `scale(${scale})`
+}
+
 function handleOpen() {
   visible.value = true
 
@@ -615,10 +569,17 @@ function handleOpen() {
 
     startSimulation()
   })
+
+  window.addEventListener('resize', updateScale)
+}
+
+const handleOpened = () => {
+  updateScale()
 }
 
 function dispose() {
   graph.value?.dispose()
+  window.removeEventListener('resize', updateScale)
 
   if (simTimer) {
     clearInterval(simTimer)
@@ -648,66 +609,76 @@ function handleClose() {
     v-model="visible"
     :show-close="false"
     @closed="dispose"
+    @opened="handleOpened"
   >
-    <div class="relative size-full overflow-hidden bg-[#0B1120] font-sans rounded-lg flex flex-col">
-      <header
-        class="relative z-20 flex items-center justify-center h-16 shrink-0 bg-[#0B1120] border-b border-[#00E5FF]/40 shadow-[0_4px_20px_rgba(0,229,255,0.15)]"
+    <div
+      ref="wrapperRef"
+      class="flex justify-center items-center w-full h-full overflow-hidden bg-transparent"
+    >
+      <div
+        ref="targetArea"
+        class="relative shrink-0 overflow-hidden bg-[#0B1120] font-sans rounded-lg flex flex-col shadow-[0_0_30px_rgba(0,229,255,0.2)]"
+        style="width: 2200px; height: 1200px; transform-origin: center center"
       >
-        <div
-          class="absolute left-10 right-10 bottom-0 h-[2px] bg-gradient-to-r from-transparent via-[#00E5FF] to-transparent opacity-80"
-        ></div>
-        <h1
-          class="text-2xl font-bold tracking-[0.2em] text-[#00E5FF] drop-shadow-[0_0_10px_rgba(0,229,255,0.8)]"
-        >
-          空气处理橇组态图
-        </h1>
-
-        <button
-          @click="handleClose"
-          class="absolute bg-transparent border-none right-6 top-1/2 -translate-y-1/2 flex items-center justify-center w-10 h-10 text-[#00E5FF]/60 hover:text-[#00E5FF] transition-all duration-300 group focus:outline-none"
-          title="关闭"
+        <header
+          class="relative z-20 flex items-center justify-center h-16 shrink-0 bg-[#0B1120] border-b border-[#00E5FF]/40 shadow-[0_4px_20px_rgba(0,229,255,0.15)]"
         >
           <div
-            class="i-ep-close w-6 h-6 relative z-10 group-hover:drop-shadow-[0_0_8px_rgba(0,229,255,0.9)] transition-all duration-300 group-hover:rotate-90"
+            class="absolute left-10 right-10 bottom-0 h-[2px] bg-gradient-to-r from-transparent via-[#00E5FF] to-transparent opacity-80"
           ></div>
-        </button>
-      </header>
-      <main class="flex-1 relative w-full h-full z-0" ref="psacontainerRef"></main>
-      <template v-if="hoveredNodeId">
-        <template v-for="dev in nodesData" :key="dev.id">
-          <div
-            v-if="dev.id === hoveredNodeId && nodeTooltip.visible && dev.params"
-            class="coord-tooltip"
-            :style="{ left: `${nodeTooltip.x}px`, top: `${nodeTooltip.y}px`, minWidth: '200px' }"
+          <h1
+            class="text-2xl font-bold tracking-[0.2em] text-[#00E5FF] drop-shadow-[0_0_10px_rgba(0,229,255,0.8)]"
+          >
+            空气处理橇组态图
+          </h1>
+
+          <button
+            @click="handleClose"
+            class="absolute bg-transparent border-none right-6 top-1/2 -translate-y-1/2 flex items-center justify-center w-10 h-10 text-[#00E5FF]/60 hover:text-[#00E5FF] transition-all duration-300 group focus:outline-none"
+            title="关闭"
           >
             <div
-              style="
-                padding-bottom: 6px;
-                margin-bottom: 4px;
-                font-size: 14px;
-                font-weight: bold;
-                color: #fff;
-                border-bottom: 1px dashed rgb(14 165 233 / 40%);
-              "
+              class="i-ep-close w-6 h-6 relative z-10 group-hover:drop-shadow-[0_0_8px_rgba(0,229,255,0.9)] transition-all duration-300 group-hover:rotate-90"
+            ></div>
+          </button>
+        </header>
+        <main class="flex-1 relative w-full h-full z-0" ref="psacontainerRef"></main>
+        <template v-if="hoveredNodeId">
+          <template v-for="dev in nodesData" :key="dev.id">
+            <div
+              v-if="dev.id === hoveredNodeId && nodeTooltip.visible && dev.params"
+              class="coord-tooltip"
+              :style="{ left: `${nodeTooltip.x}px`, top: `${nodeTooltip.y}px`, minWidth: '200px' }"
             >
-              {{ dev.name }}
-            </div>
-            <div class="coord-item">
-              <span>运行状态:</span>
-              <span
-                class="val"
-                :style="{ color: dev.status === 'running' ? '#00FF7F' : '#ef4444' }"
+              <div
+                style="
+                  padding-bottom: 6px;
+                  margin-bottom: 4px;
+                  font-size: 14px;
+                  font-weight: bold;
+                  color: #fff;
+                  border-bottom: 1px dashed rgb(14 165 233 / 40%);
+                "
               >
-                {{ dev.status === 'running' ? '运行中' : '已停机' }}
-              </span>
-            </div>
-            <div class="coord-item" v-for="(param, index) in dev.params" :key="index">
-              <span>{{ param.label }}:</span>
-              <span class="val transition-all">{{ param.value }} {{ param.unit }}</span>
+                {{ dev.name }}
+              </div>
+              <div class="coord-item">
+                <span>运行状态:</span>
+                <span
+                  class="val"
+                  :style="{ color: dev.status === 'running' ? '#00FF7F' : '#ef4444' }"
+                >
+                  {{ dev.status === 'running' ? '运行中' : '已停机' }}
+                </span>
+              </div>
+              <div class="coord-item" v-for="(param, index) in dev.params" :key="index">
+                <span>{{ param.label }}:</span>
+                <span class="val transition-all">{{ param.value }} {{ param.unit }}</span>
+              </div>
             </div>
-          </div>
+          </template>
         </template>
-      </template>
+      </div>
     </div>
   </el-dialog>
 </template>

+ 431 - 214
src/views/test/index.vue

@@ -2,7 +2,12 @@
 import { ref, reactive, onMounted, onUnmounted } from 'vue'
 import { Graph } from '@antv/x6'
 import air from './air.vue'
+import { useFullscreen } from '@vueuse/core'
+import { Crop, FullScreen } from '@element-plus/icons-vue'
 
+// 新增外层容器 Ref
+const wrapperRef = ref<HTMLDivElement>()
+const targetArea = ref<HTMLDivElement>()
 const containerRef = ref<HTMLDivElement>()
 const graph = ref<Graph | null>(null)
 let timer: number | null = null
@@ -35,8 +40,6 @@ const gasComposition = reactive([
   { name: '其他', value: 0.5, color: 'bg-slate-500' }
 ])
 
-// === 核心改造:所有设备统一数据源 (驱动面板、X6节点、Tooltip) ===
-// 我们额外存储一个 defaultParams 用于设备恢复启动时参考
 const deviceData = reactive([
   {
     id: 'ac5',
@@ -299,16 +302,14 @@ const updateDeviceBaseStats = () => {
   deviceData.forEach((dev) => {
     if (dev.status === 'running') {
       dev.params.forEach((p) => {
-        // 排除掉由工艺链计算的“处理量”、“压力”等核心参数,只对排温、负载等进行基础波动
         if (['排温', '负载率', '冲程', '冲次', '液位'].includes(p.label)) {
           const range = Math.max(0.1, p.value * 0.01)
           p.value = Number((p.value + (Math.random() - 0.5) * range).toFixed(1))
         }
       })
     } else {
-      // 停机状态下参数缓慢归零/降至环境值
       dev.params.forEach((p) => {
-        const target = p.label === '排温' ? 25 : 0 // 温度降至25度,其余归0
+        const target = p.label === '排温' ? 25 : 0
         if (p.value > target) {
           p.value = Number((p.value * 0.8 + target * 0.2).toFixed(1))
           if (Math.abs(p.value - target) < 0.2) p.value = target
@@ -320,39 +321,32 @@ const updateDeviceBaseStats = () => {
 
 const startRealtimeSimulation = () => {
   timer = window.setInterval(() => {
-    // === 第一步:计算空压机源头输出 ===
     const runningACs = deviceData.filter((d) => d.id.startsWith('ac') && d.status === 'running')
-    const totalACFlow = runningACs.length * 250 // 每台提供250单位流量
+    const totalACFlow = runningACs.length * 250
 
-    // === 第二步:空气处理橇联动 (依赖空压机) ===
     const airDevice = deviceData.find((d) => d.id === 'air')!
     const airInflow = totalACFlow
-    // 处理量:如果自身开启,则等于进气量;如果自身关闭,处理量为0
     const airOutflow = airDevice.status === 'running' ? airInflow : 0
-    airDevice.params[0].value = airOutflow // 处理量
+    airDevice.params[0].value = airOutflow
 
-    // === 第三步:制氮橇联动 (依赖空气处理) ===
     const nitDevice = deviceData.find((d) => d.id === 'nit')!
     const nitParam_Flow = nitDevice.params.find((p) => p.label === '产氮量')!
     const nitParam_Purity = nitDevice.params.find((p) => p.label === '纯度')!
 
     if (nitDevice.status === 'running' && airOutflow > 100) {
-      // 正常产氮,纯度稳定
       nitParam_Flow.value = Number((airOutflow * 0.85).toFixed(1))
       nitParam_Purity.value = Number((95.5 + (Math.random() - 0.5) * 0.2).toFixed(1))
     } else {
-      // 没气或者关机,产氮量归零,纯度下降
       nitParam_Flow.value = Number((nitParam_Flow.value * 0.7).toFixed(1))
       nitParam_Purity.value = Math.max(0, Number((nitParam_Purity.value * 0.9).toFixed(1)))
     }
 
-    // === 第四步:注水泵系统 (独立分支) ===
     const waterPumps = deviceData.filter((d) => d.id.startsWith('fillwater'))
     let totalWaterFlow = 0
     waterPumps.forEach((pump) => {
       if (pump.status === 'running') {
-        pump.params[0].value = Number((15.2 + (Math.random() - 0.5)).toFixed(1)) // 泵压
-        pump.params[1].value = Number((45 + (Math.random() - 0.5)).toFixed(1)) // 流量
+        pump.params[0].value = Number((15.2 + (Math.random() - 0.5)).toFixed(1))
+        pump.params[1].value = Number((45 + (Math.random() - 0.5)).toFixed(1))
         totalWaterFlow += pump.params[1].value
       } else {
         pump.params[0].value = Number((pump.params[0].value * 0.5).toFixed(1))
@@ -360,43 +354,34 @@ const startRealtimeSimulation = () => {
       }
     })
 
-    // === 第五步:增压橇 (综合气+水压力) ===
     const superDevice = deviceData.find((d) => d.id === 'supercharge')!
     const gasIn = nitParam_Flow.value
     const superInPress = superDevice.params.find((p) => p.label === '入口压')!
     const superOutPress = superDevice.params.find((p) => p.label === '出口压')!
 
-    // 入口压由气源决定
     superInPress.value = Number(((gasIn / 1200) * 0.8).toFixed(2))
     if (superDevice.status === 'running' && gasIn > 10) {
-      // 增压至25MPa左右
       superOutPress.value = Number((25.4 + (Math.random() - 0.5) * 0.5).toFixed(1))
     } else {
       superOutPress.value = Math.max(0, Number((superOutPress.value * 0.8).toFixed(1)))
     }
 
-    // === 第六步:大屏看板 & 井口数据联动 ===
-    // 1. 场站总览
     overviewData.totalFlow = Math.floor(gasIn)
     overviewData.pressure = superOutPress.value
     overviewData.efficiency = gasIn > 0 ? Number((90 + Math.random() * 5).toFixed(1)) : 0
     overviewData.temperature = gasIn > 0 ? Number((35 + Math.random() * 5).toFixed(1)) : 25
 
-    // 2. 井口数据
     wellData.wellheadPressure = Number((overviewData.pressure * 0.98).toFixed(1))
     if (wellData.wellheadPressure > 2) {
-      // 只有压力够才算注气成功
       wellData.dailyInjection += Math.floor(gasIn / 3600)
       wellData.cumulative = Number((wellData.cumulative + 0.0001).toFixed(4))
     }
 
-    // 3. 气体成分随产量微调 (模拟)
     if (gasIn > 10) {
       gasComposition[0].value = Number((95 + (Math.random() - 0.5) * 0.5).toFixed(1))
       gasComposition[1].value = Number((100 - gasComposition[0].value - 2).toFixed(1))
     }
 
-    // 处理排温等基础属性波动
     updateDeviceBaseStats()
   }, 1000)
 }
@@ -688,260 +673,326 @@ const renderNodes = () => {
   })
 }
 
+// === 大屏全局等比缩放计算逻辑 ===
+const updateScale = () => {
+  if (!wrapperRef.value || !targetArea.value) return
+
+  const currentWidth = wrapperRef.value.clientWidth
+  const currentHeight = wrapperRef.value.clientHeight
+
+  // 固定你的设计稿标准尺寸
+  const designWidth = 2560
+  const designHeight = 1440
+
+  const scaleX = currentWidth / designWidth
+  const scaleY = currentHeight / designHeight
+  // 取最小比例,确保内容完整显示(类似 object-fit: contain)
+  const scale = Math.min(scaleX, scaleY)
+
+  targetArea.value.style.transform = `scale(${scale})`
+}
+
 onMounted(() => {
   initGraph()
   renderGroundAndFence()
   renderEdges()
   renderNodes()
 
+  // 你完美调试出的硬编码参数被成功复用
   if (graph.value) {
-    graph.value.zoomTo(0.35)
-    graph.value.translate(437, 530.65)
+    graph.value.zoomTo(0.4)
+    graph.value.translate(521.71, 704.89)
   }
+
   startRealtimeSimulation()
+
+  // 初始化缩放并监听窗口调整
+  updateScale()
+  window.addEventListener('resize', updateScale)
 })
 
 onUnmounted(() => {
   graph.value?.dispose()
   if (timer) clearInterval(timer)
+  window.removeEventListener('resize', updateScale)
 })
+
+const { toggle, isFullscreen } = useFullscreen(wrapperRef)
 </script>
 
 <template>
   <div
-    class="relative flex flex-col w-full overflow-hidden bg-[#0B1120] font-sans"
+    ref="wrapperRef"
+    id="screen-wrapper"
+    class="flex justify-center items-center w-full overflow-hidden bg-[#0B1120]"
     style="
       height: calc(
         100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
       );
     "
   >
-    <header
-      class="relative z-20 flex items-center justify-center h-16 shrink-0 bg-[#0B1120] border-b border-[#00E5FF]/40 shadow-[0_4px_20px_rgba(0,229,255,0.15)]"
+    <div
+      ref="targetArea"
+      class="relative flex flex-col shrink-0 overflow-hidden bg-[#0B1120] font-sans"
+      style="width: 2560px; height: 1440px; transform-origin: center center"
     >
-      <div
-        class="absolute left-10 right-10 bottom-0 h-[2px] bg-gradient-to-r from-transparent via-[#00E5FF] to-transparent opacity-80"
-      ></div>
-      <h1
-        class="text-2xl font-bold tracking-[0.2em] text-[#00E5FF] drop-shadow-[0_0_10px_rgba(0,229,255,0.8)]"
+      <!-- style="width: 2300px; height: 1147px; transform-origin: center center" -->
+      <header
+        class="relative z-20 flex items-center justify-center h-16 shrink-0 bg-[#0B1120] border-b border-[#00E5FF]/40 shadow-[0_4px_20px_rgba(0,229,255,0.15)]"
       >
-        XX注气场站注气工艺组态图
-      </h1>
-    </header>
-
-    <div class="relative flex-1 w-full h-full overflow-hidden">
-      <main class="absolute inset-0 z-0" ref="containerRef"></main>
-
-      <div class="absolute inset-0 z-10 pointer-events-none flex justify-between p-6">
-        <div class="flex flex-col gap-6 w-[380px] pointer-events-auto h-full pb-2">
-          <div
-            class="bg-[#0A1628]/95 border border-[#00E5FF]/60 shadow-[0_0_15px_rgba(0,229,255,0.15)_inset] rounded-lg p-5 flex flex-col backdrop-blur-md transition-all shrink-0"
-          >
-            <h2
-              class="text-lg font-bold text-[#00E5FF] border-b border-[#00E5FF]/30 pb-2 mb-4 drop-shadow-[0_0_5px_rgba(0,229,255,0.8)]"
-              >场站实时总览</h2
+        <div
+          class="absolute left-10 right-10 bottom-0 h-[2px] bg-gradient-to-r from-transparent via-[#00E5FF] to-transparent opacity-80"
+        ></div>
+        <h1
+          class="text-2xl font-bold tracking-[0.2em] text-[#00E5FF] drop-shadow-[0_0_10px_rgba(0,229,255,0.8)]"
+        >
+          XX注气场站注气工艺组态图
+        </h1>
+        <el-button
+          size="default"
+          class="absolute right-6"
+          :type="isFullscreen ? 'info' : 'primary'"
+          :icon="isFullscreen ? Crop : FullScreen"
+          @click="toggle"
+        >
+          {{ isFullscreen ? '退出全屏' : '全屏' }}
+        </el-button>
+      </header>
+
+      <div class="relative flex-1 w-full h-full overflow-hidden">
+        <main class="absolute inset-0 z-0" ref="containerRef"></main>
+
+        <div class="absolute inset-0 z-10 pointer-events-none flex justify-between p-6">
+          <div class="flex flex-col gap-6 w-[380px] pointer-events-auto h-full pb-2">
+            <div
+              class="bg-[#0A1628]/95 border border-[#00E5FF]/60 shadow-[0_0_15px_rgba(0,229,255,0.15)_inset] rounded-lg p-5 flex flex-col backdrop-blur-md transition-all shrink-0"
             >
-            <div class="grid grid-cols-2 gap-4">
-              <div class="bg-[#112240] border border-[#1E3A8A] rounded p-3 flex flex-col">
-                <span class="text-xs text-slate-300 font-medium mb-1">注气总流量 (Nm³/h)</span>
-                <span
-                  class="text-xl font-bold font-mono text-[#00FF7F] drop-shadow-[0_0_5px_rgba(0,255,127,0.5)] transition-all"
-                  >{{ overviewData.totalFlow.toLocaleString() }}</span
-                >
-              </div>
-              <div class="bg-[#112240] border border-[#1E3A8A] rounded p-3 flex flex-col">
-                <span class="text-xs text-slate-300 font-medium mb-1">管网压力 (MPa)</span>
-                <span
-                  class="text-xl font-bold font-mono text-[#38BDF8] drop-shadow-[0_0_5px_rgba(56,189,248,0.5)] transition-all"
-                  >{{ overviewData.pressure }}</span
-                >
-              </div>
-              <div class="bg-[#112240] border border-[#1E3A8A] rounded p-3 flex flex-col">
-                <span class="text-xs text-slate-300 font-medium mb-1">排气温度 (°C)</span>
-                <span
-                  class="text-xl font-bold font-mono text-[#FBBF24] drop-shadow-[0_0_5px_rgba(251,191,36,0.5)] transition-all"
-                  >{{ overviewData.temperature }}</span
-                >
-              </div>
-              <div class="bg-[#112240] border border-[#1E3A8A] rounded p-3 flex flex-col">
-                <span class="text-xs text-slate-300 font-medium mb-1">系统能效 (%)</span>
-                <span
-                  class="text-xl font-bold font-mono text-[#00E5FF] drop-shadow-[0_0_5px_rgba(0,229,255,0.5)] transition-all"
-                  >{{ overviewData.efficiency }}</span
-                >
+              <h2
+                class="text-lg font-bold text-[#00E5FF] border-b border-[#00E5FF]/30 pb-2 mb-4 drop-shadow-[0_0_5px_rgba(0,229,255,0.8)]"
+                >场站实时总览</h2
+              >
+              <div class="grid grid-cols-2 gap-4">
+                <div class="bg-[#112240] border border-[#1E3A8A] rounded p-3 flex flex-col">
+                  <span class="text-xs text-slate-300 font-medium mb-1">注气总流量 (Nm³/h)</span>
+                  <span
+                    class="text-xl font-bold font-mono text-[#00FF7F] drop-shadow-[0_0_5px_rgba(0,255,127,0.5)] transition-all"
+                    >{{ overviewData.totalFlow.toLocaleString() }}</span
+                  >
+                </div>
+                <div class="bg-[#112240] border border-[#1E3A8A] rounded p-3 flex flex-col">
+                  <span class="text-xs text-slate-300 font-medium mb-1">管网压力 (MPa)</span>
+                  <span
+                    class="text-xl font-bold font-mono text-[#38BDF8] drop-shadow-[0_0_5px_rgba(56,189,248,0.5)] transition-all"
+                    >{{ overviewData.pressure }}</span
+                  >
+                </div>
+                <div class="bg-[#112240] border border-[#1E3A8A] rounded p-3 flex flex-col">
+                  <span class="text-xs text-slate-300 font-medium mb-1">排气温度 (°C)</span>
+                  <span
+                    class="text-xl font-bold font-mono text-[#FBBF24] drop-shadow-[0_0_5px_rgba(251,191,36,0.5)] transition-all"
+                    >{{ overviewData.temperature }}</span
+                  >
+                </div>
+                <div class="bg-[#112240] border border-[#1E3A8A] rounded p-3 flex flex-col">
+                  <span class="text-xs text-slate-300 font-medium mb-1">系统能效 (%)</span>
+                  <span
+                    class="text-xl font-bold font-mono text-[#00E5FF] drop-shadow-[0_0_5px_rgba(0,229,255,0.5)] transition-all"
+                    >{{ overviewData.efficiency }}</span
+                  >
+                </div>
               </div>
             </div>
-          </div>
 
-          <div
-            class="bg-[#0A1628]/95 border border-[#00E5FF]/60 shadow-[0_0_15px_rgba(0,229,255,0.15)_inset] rounded-lg p-5 flex flex-col flex-1 min-h-0 backdrop-blur-md"
-          >
-            <h2
-              class="text-lg font-bold text-[#00E5FF] border-b border-[#00E5FF]/30 pb-2 mb-4 drop-shadow-[0_0_5px_rgba(0,229,255,0.8)] shrink-0"
-              >全场设备状态监控</h2
+            <div
+              class="bg-[#0A1628]/95 border border-[#00E5FF]/60 shadow-[0_0_15px_rgba(0,229,255,0.15)_inset] rounded-lg p-5 flex flex-col flex-1 min-h-0 backdrop-blur-md"
             >
-
-            <div class="flex-1 flex flex-col gap-3 overflow-y-auto pr-2 custom-scrollbar">
-              <div
-                v-for="dev in deviceData"
-                :key="dev.id"
-                class="flex items-center justify-between p-3 bg-[#112240] border border-[#1E3A8A] rounded hover:border-[#00E5FF]/60 transition-colors"
+              <h2
+                class="text-lg font-bold text-[#00E5FF] border-b border-[#00E5FF]/30 pb-2 mb-4 drop-shadow-[0_0_5px_rgba(0,229,255,0.8)] shrink-0"
+                >全场设备状态监控</h2
               >
-                <div class="flex items-center gap-3">
-                  <div
-                    :class="[
-                      'w-3 h-3 rounded-full shadow-[0_0_8px] transition-colors shrink-0',
-                      dev.status === 'running'
-                        ? 'bg-[#00FF7F] shadow-[#00FF7F]'
-                        : 'bg-[#EF4444] shadow-[#EF4444]'
-                    ]"
-                  ></div>
-                  <span class="text-sm font-bold text-white w-[75px] truncate" :title="dev.name">{{
-                    dev.name
-                  }}</span>
-                </div>
 
-                <div class="flex gap-3 text-sm font-mono min-w-[120px] justify-end items-center">
-                  <div class="flex gap-3 min-w-[90px] justify-end">
+              <div class="flex-1 flex flex-col gap-3 overflow-y-auto pr-2 custom-scrollbar">
+                <div
+                  v-for="dev in deviceData"
+                  :key="dev.id"
+                  class="flex items-center justify-between p-3 bg-[#112240] border border-[#1E3A8A] rounded hover:border-[#00E5FF]/60 transition-colors"
+                >
+                  <div class="flex items-center gap-3">
                     <div
-                      v-for="(param, idx) in dev.params.slice(0, 2)"
-                      :key="idx"
-                      class="flex flex-col items-end"
+                      :class="[
+                        'w-3 h-3 rounded-full shadow-[0_0_8px] transition-colors shrink-0',
+                        dev.status === 'running'
+                          ? 'bg-[#00FF7F] shadow-[#00FF7F]'
+                          : 'bg-[#EF4444] shadow-[#EF4444]'
+                      ]"
+                    ></div>
+                    <span
+                      class="text-sm font-bold text-white w-[75px] truncate"
+                      :title="dev.name"
+                      >{{ dev.name }}</span
                     >
-                      <span class="text-slate-400 text-[10px]">{{ param.label }}</span>
-                      <span
-                        :class="
-                          dev.status === 'running'
-                            ? 'text-[#00E5FF] font-bold transition-all'
-                            : 'text-slate-500'
-                        "
+                  </div>
+
+                  <div class="flex gap-3 text-sm font-mono min-w-[120px] justify-end items-center">
+                    <div class="flex gap-3 min-w-[90px] justify-end">
+                      <div
+                        v-for="(param, idx) in dev.params.slice(0, 2)"
+                        :key="idx"
+                        class="flex flex-col items-end"
                       >
-                        {{ param.value }}<span class="text-[10px] ml-0.5">{{ param.unit }}</span>
-                      </span>
+                        <span class="text-slate-400 text-[10px]">{{ param.label }}</span>
+                        <span
+                          :class="
+                            dev.status === 'running'
+                              ? 'text-[#00E5FF] font-bold transition-all'
+                              : 'text-slate-500'
+                          "
+                        >
+                          {{ param.value }}<span class="text-[10px] ml-0.5">{{ param.unit }}</span>
+                        </span>
+                      </div>
                     </div>
-                  </div>
-                  <button
-                    @click="toggleDevice(dev)"
-                    class="ml-2 p-1.5 rounded flex items-center justify-center transition-all duration-300 border focus:outline-none active:scale-90"
-                    :class="
-                      dev.status === 'running'
-                        ? 'border-[#EF4444]/30 text-[#EF4444] hover:bg-[#EF4444]/20 hover:border-[#EF4444] hover:shadow-[0_0_10px_rgba(239,68,68,0.5)]'
-                        : 'border-[#00FF7F]/30 text-[#00FF7F] hover:bg-[#00FF7F]/20 hover:border-[#00FF7F] hover:shadow-[0_0_10px_rgba(0,255,127,0.5)]'
-                    "
-                    :title="dev.status === 'running' ? '停止设备' : '启动设备'"
-                  >
-                    <div
-                      :class="dev.status === 'running' ? 'i-mdi-pause' : 'i-mdi-play'"
-                      class="w-4 h-4"
+                    <!-- <button
+                      @click="toggleDevice(dev)"
+                      class="ml-2 p-1.5 rounded flex items-center justify-center transition-all duration-300 border focus:outline-none active:scale-90"
+                      :class="
+                        dev.status === 'running'
+                          ? 'border-[#EF4444]/30 text-[#EF4444] hover:bg-[#EF4444]/20 hover:border-[#EF4444] hover:shadow-[0_0_10px_rgba(239,68,68,0.5)]'
+                          : 'border-[#00FF7F]/30 text-[#00FF7F] hover:bg-[#00FF7F]/20 hover:border-[#00FF7F] hover:shadow-[0_0_10px_rgba(0,255,127,0.5)]'
+                      "
+                      :title="dev.status === 'running' ? '停止设备' : '启动设备'"
                     >
+                      <div
+                        :class="dev.status === 'running' ? 'i-mdi-pause' : 'i-mdi-play'"
+                        class="w-4 h-4"
+                      >
+                      </div>
+                    </button> -->
+                    <div class="switch-container">
+                      <label class="switch">
+                        <input
+                          type="checkbox"
+                          :checked="dev.status === 'running'"
+                          @change="toggleDevice(dev)"
+                        />
+                        <div class="button">
+                          <div class="light"></div>
+                          <div class="dots"></div>
+                          <div class="characters"></div>
+                          <div class="shine"></div>
+                          <div class="shadow"></div>
+                        </div>
+                      </label>
                     </div>
-                  </button>
+                  </div>
                 </div>
               </div>
             </div>
           </div>
-        </div>
 
-        <div class="flex flex-col gap-6 w-[380px] pointer-events-auto h-full pb-2">
-          <div
-            class="bg-[#0A1628]/95 border border-[#00E5FF]/60 shadow-[0_0_15px_rgba(0,229,255,0.15)_inset] rounded-lg p-5 flex flex-col backdrop-blur-md shrink-0"
-          >
-            <h2
-              class="text-lg font-bold text-[#00E5FF] border-b border-[#00E5FF]/30 pb-2 mb-4 drop-shadow-[0_0_5px_rgba(0,229,255,0.8)]"
-              >注气井实时数据</h2
+          <div class="flex flex-col gap-6 w-[380px] pointer-events-auto h-full pb-2">
+            <div
+              class="bg-[#0A1628]/95 border border-[#00E5FF]/60 shadow-[0_0_15px_rgba(0,229,255,0.15)_inset] rounded-lg p-5 flex flex-col backdrop-blur-md shrink-0"
             >
-            <div class="flex flex-col gap-4">
-              <div
-                class="flex justify-between items-end bg-[#112240] p-3 rounded border border-[#1E3A8A]"
+              <h2
+                class="text-lg font-bold text-[#00E5FF] border-b border-[#00E5FF]/30 pb-2 mb-4 drop-shadow-[0_0_5px_rgba(0,229,255,0.8)]"
+                >注气井实时数据</h2
               >
-                <span class="text-white text-sm font-medium">井口压力</span>
-                <span class="text-2xl font-mono text-[#00E5FF] font-bold transition-all"
-                  >{{ wellData.wellheadPressure
-                  }}<span class="text-sm text-slate-400">MPa</span></span
+              <div class="flex flex-col gap-4">
+                <div
+                  class="flex justify-between items-end bg-[#112240] p-3 rounded border border-[#1E3A8A]"
                 >
-              </div>
-              <div
-                class="flex justify-between items-end bg-[#112240] p-3 rounded border border-[#1E3A8A]"
-              >
-                <span class="text-white text-sm font-medium">日注气量</span>
-                <span class="text-2xl font-mono text-[#00FF7F] font-bold transition-all"
-                  >{{ wellData.dailyInjection.toLocaleString()
-                  }}<span class="text-sm text-slate-400">Nm³</span></span
+                  <span class="text-white text-sm font-medium">井口压力</span>
+                  <span class="text-2xl font-mono text-[#00E5FF] font-bold transition-all"
+                    >{{ wellData.wellheadPressure
+                    }}<span class="text-sm text-slate-400">MPa</span></span
+                  >
+                </div>
+                <div
+                  class="flex justify-between items-end bg-[#112240] p-3 rounded border border-[#1E3A8A]"
                 >
-              </div>
-              <div
-                class="flex justify-between items-end bg-[#112240] p-3 rounded border border-[#1E3A8A]"
-              >
-                <span class="text-white text-sm font-medium">累计注气</span>
-                <span class="text-2xl font-mono text-[#FBBF24] font-bold transition-all"
-                  >{{ wellData.cumulative }} <span class="text-sm text-slate-400">万Nm³</span></span
+                  <span class="text-white text-sm font-medium">日注气量</span>
+                  <span class="text-2xl font-mono text-[#00FF7F] font-bold transition-all"
+                    >{{ wellData.dailyInjection.toLocaleString()
+                    }}<span class="text-sm text-slate-400">Nm³</span></span
+                  >
+                </div>
+                <div
+                  class="flex justify-between items-end bg-[#112240] p-3 rounded border border-[#1E3A8A]"
                 >
+                  <span class="text-white text-sm font-medium">累计注气</span>
+                  <span class="text-2xl font-mono text-[#FBBF24] font-bold transition-all"
+                    >{{ wellData.cumulative }}
+                    <span class="text-sm text-slate-400">万Nm³</span></span
+                  >
+                </div>
               </div>
             </div>
-          </div>
 
-          <div
-            class="bg-[#0A1628]/95 border border-[#00E5FF]/60 shadow-[0_0_15px_rgba(0,229,255,0.15)_inset] rounded-lg p-5 flex flex-col backdrop-blur-md shrink-0"
-          >
-            <h2
-              class="text-lg font-bold text-[#00E5FF] border-b border-[#00E5FF]/30 pb-2 mb-4 drop-shadow-[0_0_5px_rgba(0,229,255,0.8)]"
-              >气体成分占比</h2
+            <div
+              class="bg-[#0A1628]/95 border border-[#00E5FF]/60 shadow-[0_0_15px_rgba(0,229,255,0.15)_inset] rounded-lg p-5 flex flex-col backdrop-blur-md shrink-0"
             >
-            <div class="flex flex-col gap-4">
-              <div v-for="gas in gasComposition" :key="gas.name" class="flex flex-col gap-2">
-                <div class="flex justify-between text-sm">
-                  <span class="text-white font-medium">{{ gas.name }}</span>
-                  <span class="text-[#00E5FF] font-mono font-bold transition-all"
-                    >{{ gas.value }}%</span
-                  >
-                </div>
-                <div class="w-full h-2 bg-[#1E3A8A] rounded-full overflow-hidden">
-                  <div
-                    :class="[
-                      'h-full rounded-full transition-all duration-500 ease-in-out',
-                      gas.color
-                    ]"
-                    :style="{ width: `${gas.value}%` }"
-                  ></div>
+              <h2
+                class="text-lg font-bold text-[#00E5FF] border-b border-[#00E5FF]/30 pb-2 mb-4 drop-shadow-[0_0_5px_rgba(0,229,255,0.8)]"
+                >气体成分占比</h2
+              >
+              <div class="flex flex-col gap-4">
+                <div v-for="gas in gasComposition" :key="gas.name" class="flex flex-col gap-2">
+                  <div class="flex justify-between text-sm">
+                    <span class="text-white font-medium">{{ gas.name }}</span>
+                    <span class="text-[#00E5FF] font-mono font-bold transition-all"
+                      >{{ gas.value }}%</span
+                    >
+                  </div>
+                  <div class="w-full h-2 bg-[#1E3A8A] rounded-full overflow-hidden">
+                    <div
+                      :class="[
+                        'h-full rounded-full transition-all duration-500 ease-in-out',
+                        gas.color
+                      ]"
+                      :style="{ width: `${gas.value}%` }"
+                    ></div>
+                  </div>
                 </div>
               </div>
             </div>
-          </div>
 
-          <div
-            class="bg-[#0A1628]/95 border border-[#00E5FF]/60 shadow-[0_0_15px_rgba(0,229,255,0.15)_inset] rounded-lg p-5 flex flex-col flex-1 min-h-0 backdrop-blur-md"
-          >
-            <h2
-              class="text-lg font-bold text-[#00E5FF] border-b border-[#00E5FF]/30 pb-2 mb-4 drop-shadow-[0_0_5px_rgba(0,229,255,0.8)] shrink-0"
-              >系统操作/告警日志</h2
+            <div
+              class="bg-[#0A1628]/95 border border-[#00E5FF]/60 shadow-[0_0_15px_rgba(0,229,255,0.15)_inset] rounded-lg p-5 flex flex-col flex-1 min-h-0 backdrop-blur-md"
             >
-            <div class="flex-1 flex flex-col gap-3 overflow-y-auto pr-2 custom-scrollbar">
-              <div
-                v-for="(log, idx) in alarmLogs"
-                :key="idx"
-                class="flex flex-col p-3 bg-[#112240] rounded border-l-4"
-                :class="log.level === 'warn' ? 'border-[#FBBF24]' : 'border-[#00E5FF]'"
+              <h2
+                class="text-lg font-bold text-[#00E5FF] border-b border-[#00E5FF]/30 pb-2 mb-4 drop-shadow-[0_0_5px_rgba(0,229,255,0.8)] shrink-0"
+                >系统操作/告警日志</h2
               >
-                <div class="flex justify-between items-center mb-2">
-                  <span class="text-xs text-slate-400 font-mono">{{ log.time }}</span>
-                  <span
-                    :class="[
-                      'text-xs px-2 py-0.5 rounded font-bold',
-                      log.level === 'warn'
-                        ? 'bg-[#FBBF24]/20 text-[#FBBF24]'
-                        : 'bg-[#00E5FF]/20 text-[#00E5FF]'
-                    ]"
-                  >
-                    {{ log.level === 'warn' ? '警告' : '信息' }}
-                  </span>
+              <div class="flex-1 flex flex-col gap-3 overflow-y-auto pr-2 custom-scrollbar">
+                <div
+                  v-for="(log, idx) in alarmLogs"
+                  :key="idx"
+                  class="flex flex-col p-3 bg-[#112240] rounded border-l-4"
+                  :class="log.level === 'warn' ? 'border-[#FBBF24]' : 'border-[#00E5FF]'"
+                >
+                  <div class="flex justify-between items-center mb-2">
+                    <span class="text-xs text-slate-400 font-mono">{{ log.time }}</span>
+                    <span
+                      :class="[
+                        'text-xs px-2 py-0.5 rounded font-bold',
+                        log.level === 'warn'
+                          ? 'bg-[#FBBF24]/20 text-[#FBBF24]'
+                          : 'bg-[#00E5FF]/20 text-[#00E5FF]'
+                      ]"
+                    >
+                      {{ log.level === 'warn' ? '警告' : '信息' }}
+                    </span>
+                  </div>
+                  <span class="text-sm text-white">{{ log.msg }}</span>
                 </div>
-                <span class="text-sm text-white">{{ log.msg }}</span>
               </div>
             </div>
           </div>
         </div>
       </div>
     </div>
+    <air ref="airDialogRef" />
   </div>
 
   <template v-if="hoveredNodeId">
@@ -976,8 +1027,6 @@ onUnmounted(() => {
       </div>
     </template>
   </template>
-
-  <air ref="airDialogRef" />
 </template>
 
 <style scoped>
@@ -1034,6 +1083,174 @@ onUnmounted(() => {
 .custom-scrollbar::-webkit-scrollbar-thumb:hover {
   background: rgb(0 229 255 / 100%);
 }
+
+.switch-container {
+  display: inline-block;
+  width: 37.5px; /* 约 150 * 0.12 */
+  height: 48.75px; /* 约 195 * 0.12 */
+  overflow: visible;
+}
+
+.switch {
+  display: block;
+  width: 150px;
+  height: 195px;
+  padding: 20px;
+  background-color: black;
+  border-radius: 5px;
+  transform: scale(0.25);
+  box-shadow:
+    0 0 10px 2px rgb(0 0 0 / 20%),
+    0 0 1px 2px black,
+    inset 0 2px 2px -2px white,
+    inset 0 0 2px 15px #47434c,
+    inset 0 0 2px 22px black;
+  perspective: 700px;
+  transform-origin: 0 0; /* 锁定左上角缩放 */
+}
+
+.switch input {
+  display: none;
+}
+
+.switch input:checked + .button {
+  transform: translateZ(20px) rotateX(25deg);
+  box-shadow: 0 -10px 20px #ff1818;
+}
+
+.switch input:checked + .button .light {
+  animation: flicker 0.2s infinite 0.3s;
+}
+
+.switch input:checked + .button .shine {
+  opacity: 1;
+}
+
+.switch input:checked + .button .shadow {
+  opacity: 0;
+}
+
+.switch .button {
+  position: relative;
+  display: block;
+  height: 100%;
+  cursor: pointer;
+  background: linear-gradient(#980000 0%, #6f0000 30%, #6f0000 70%, #980000 100%);
+  background-color: #9b0621;
+  background-repeat: no-repeat;
+  transform: translateZ(20px) rotateX(-25deg);
+  transition: all 0.3s cubic-bezier(1, 0, 1, 1);
+  transform-origin: center center -20px;
+  transform-style: preserve-3d;
+}
+
+.switch .button::before {
+  position: absolute;
+  top: 0;
+  width: 100%;
+  height: 50px;
+  background:
+    linear-gradient(rgb(255 255 255 / 80%) 10%, rgb(255 255 255 / 30%) 30%, #650000 75%, #320000)
+      50% 50%/97% 97%,
+    #b10000;
+  background-repeat: no-repeat;
+  content: '';
+  transform: rotateX(-90deg);
+  transform-origin: top;
+}
+
+.switch .button::after {
+  position: absolute;
+  bottom: 0;
+  width: 100%;
+  height: 50px;
+  background-image: linear-gradient(#650000, #320000);
+  content: '';
+  transform: translateY(50px) rotateX(-90deg);
+  box-shadow:
+    0 50px 8px 0 black,
+    0 80px 20px 0 rgb(0 0 0 / 50%);
+  transform-origin: top;
+}
+
+.switch .light {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  background-image: radial-gradient(#ffc97e, #ff1818 40%, transparent 70%);
+  opacity: 0;
+  animation: light-off 1s;
+}
+
+.switch .dots {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  background-image: radial-gradient(transparent 30%, rgb(101 0 0 / 70%) 70%);
+  background-size: 10px 10px;
+}
+
+.switch .characters {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  background:
+    linear-gradient(white, white) 50% 20%/5% 20%,
+    radial-gradient(circle, transparent 50%, white 52%, white 70%, transparent 72%) 50% 80%/33% 25%;
+  background-repeat: no-repeat;
+}
+
+.switch .shine {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  background:
+    linear-gradient(white, transparent 3%) 50% 50%/97% 97%,
+    linear-gradient(
+        rgb(255 255 255 / 50%),
+        transparent 50%,
+        transparent 80%,
+        rgb(255 255 255 / 50%)
+      )
+      50% 50%/97% 97%;
+  background-repeat: no-repeat;
+  opacity: 0.3;
+  transition: all 0.3s cubic-bezier(1, 0, 1, 1);
+}
+
+.switch .shadow {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  background: linear-gradient(transparent 70%, rgb(0 0 0 / 80%));
+  background-repeat: no-repeat;
+  opacity: 1;
+  transition: all 0.3s cubic-bezier(1, 0, 1, 1);
+}
+
+@keyframes flicker {
+  0% {
+    opacity: 1;
+  }
+
+  80% {
+    opacity: 0.8;
+  }
+
+  100% {
+    opacity: 1;
+  }
+}
+
+@keyframes light-off {
+  0% {
+    opacity: 1;
+  }
+
+  80% {
+    opacity: 0;
+  }
+}
 </style>
 
 <style>