Просмотр исходного кода

根据 原型UI 重构了项目

dedsudiyu 1 неделя назад
Родитель
Сommit
582725b64e
38 измененных файлов с 1621 добавлено и 1063 удалено
  1. 2 0
      .gitignore
  2. 0 1
      dist/css/104.8a61ab45.css
  3. 0 1
      dist/css/317.8710075d.css
  4. 0 1
      dist/css/688.642d3948.css
  5. 0 1
      dist/css/app.a0efcfb9.css
  6. 0 1
      dist/css/chunk-vendors.10dd4e95.css
  7. BIN
      dist/fonts/element-icons.f1a45d74.ttf
  8. BIN
      dist/fonts/element-icons.ff18efd1.woff
  9. 0 1
      dist/index.html
  10. 0 12
      dist/js/104.bcb8d5f3.js
  11. 0 1
      dist/js/104.bcb8d5f3.js.map
  12. 0 2
      dist/js/317.77209586.js
  13. 0 1
      dist/js/317.77209586.js.map
  14. 0 2
      dist/js/688.86d89ace.js
  15. 0 1
      dist/js/688.86d89ace.js.map
  16. 0 2
      dist/js/884.c64fa447.js
  17. 0 1
      dist/js/884.c64fa447.js.map
  18. 0 2
      dist/js/app.a8f798a9.js
  19. 0 1
      dist/js/app.a8f798a9.js.map
  20. 0 55
      dist/js/chunk-vendors.73b7a99d.js
  21. 0 1
      dist/js/chunk-vendors.73b7a99d.js.map
  22. 25 4
      src/App.vue
  23. 77 41
      src/api/index.js
  24. 291 0
      src/components/AlertModal.vue
  25. 82 78
      src/components/Header.vue
  26. 71 115
      src/components/LabStats/BasicStats.vue
  27. 86 98
      src/components/LabStats/DeviceStats.vue
  28. 84 110
      src/components/LabStats/EnvSensing.vue
  29. 88 90
      src/components/LabStats/EquipmentStats.vue
  30. 64 75
      src/components/LabStats/PersonStats.vue
  31. 69 83
      src/components/LabStats/RiskWarning.vue
  32. 45 91
      src/components/LabStats/SafetyLevel.vue
  33. 74 31
      src/components/VideoMonitor/BuildingNav.vue
  34. 243 59
      src/components/VideoMonitor/VideoGrid.vue
  35. 150 44
      src/styles/global.scss
  36. 29 16
      src/styles/variables.scss
  37. 40 28
      src/views/LabStatus.vue
  38. 101 13
      src/views/VideoMonitor.vue

+ 2 - 0
.gitignore

@@ -1,3 +1,5 @@
 /node_modules
 /node_modules
 node_modules
 node_modules
 call-word
 call-word
+**/.DS_Store
+dist

Разница между файлами не показана из-за своего большого размера
+ 0 - 1
dist/css/104.8a61ab45.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 1
dist/css/317.8710075d.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 1
dist/css/688.642d3948.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 1
dist/css/app.a0efcfb9.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 1
dist/css/chunk-vendors.10dd4e95.css


BIN
dist/fonts/element-icons.f1a45d74.ttf


BIN
dist/fonts/element-icons.ff18efd1.woff


Разница между файлами не показана из-за своего большого размера
+ 0 - 1
dist/index.html


Разница между файлами не показана из-за своего большого размера
+ 0 - 12
dist/js/104.bcb8d5f3.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 1
dist/js/104.bcb8d5f3.js.map


Разница между файлами не показана из-за своего большого размера
+ 0 - 2
dist/js/317.77209586.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 1
dist/js/317.77209586.js.map


Разница между файлами не показана из-за своего большого размера
+ 0 - 2
dist/js/688.86d89ace.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 1
dist/js/688.86d89ace.js.map


Разница между файлами не показана из-за своего большого размера
+ 0 - 2
dist/js/884.c64fa447.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 1
dist/js/884.c64fa447.js.map


Разница между файлами не показана из-за своего большого размера
+ 0 - 2
dist/js/app.a8f798a9.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 1
dist/js/app.a8f798a9.js.map


Разница между файлами не показана из-за своего большого размера
+ 0 - 55
dist/js/chunk-vendors.73b7a99d.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 1
dist/js/chunk-vendors.73b7a99d.js.map


+ 25 - 4
src/App.vue

@@ -1,18 +1,39 @@
 <template>
 <template>
   <div id="app">
   <div id="app">
-    <router-view />
+    <div class="viewport">
+      <div class="screen" ref="screen">
+        <router-view />
+      </div>
+    </div>
   </div>
   </div>
 </template>
 </template>
 
 
 <script>
 <script>
 export default {
 export default {
-  name: 'App'
+  name: 'App',
+  mounted() {
+    this.fitScreen()
+    window.addEventListener('resize', this.fitScreen)
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.fitScreen)
+  },
+  methods: {
+    fitScreen() {
+      const scr = this.$refs.screen
+      if (!scr) return
+      const sw = 1920, sh = 1080
+      const vw = window.innerWidth, vh = window.innerHeight
+      const sx = vw / sw, sy = vh / sh
+      scr.style.transform = `translate(-50%, -50%) scale(${sx}, ${sy})`
+    }
+  }
 }
 }
 </script>
 </script>
 
 
 <style lang="scss">
 <style lang="scss">
 #app {
 #app {
-  width: 1920px;
-  height: 1080px;
+  width: 100%;
+  height: 100%;
 }
 }
 </style>
 </style>

+ 77 - 41
src/api/index.js

@@ -43,6 +43,8 @@ export function getLabBasicStats() {
 export function getSafetyLevelStats() {
 export function getSafetyLevelStats() {
   return mockDelay({
   return mockDelay({
     departments: [
     departments: [
+      { name: '安全技术\n研究所', total: 11, level1: 3, level2: 3, level3: 3, level4: 2 },
+      { name: '职业安全\n研究所', total: 9, level1: 2, level2: 3, level3: 2, level4: 2 },
       { name: '化学品安全\n研究所', total: 12, level1: 3, level2: 4, level3: 3, level4: 2 },
       { name: '化学品安全\n研究所', total: 12, level1: 3, level2: 4, level3: 3, level4: 2 },
       { name: '矿山安全\n研究所', total: 10, level1: 2, level2: 3, level3: 3, level4: 2 },
       { name: '矿山安全\n研究所', total: 10, level1: 2, level2: 3, level3: 3, level4: 2 },
       { name: '应急科学\n研究中心', total: 14, level1: 4, level2: 4, level3: 3, level4: 3 },
       { name: '应急科学\n研究中心', total: 14, level1: 4, level2: 4, level3: 3, level4: 3 },
@@ -96,12 +98,12 @@ export function getRiskWarningList() {
   return mockDelay({
   return mockDelay({
     totalMonth: 12,
     totalMonth: 12,
     list: [
     list: [
-      { id: 1, lab: '高温高压实验室C102(C102)', dept: '矿山安全研究所', type: '温度', value: '35.6°C', time: '2026-03-04 14:28:05' },
-      { id: 2, lab: '气体分析实验室A305(A305)', dept: '职业安全研究所', type: 'CO₂', value: '1050ppm', time: '2026-03-04 13:55:42' },
-      { id: 3, lab: '气体分析实验室A304(A304)', dept: '职业安全研究所', type: 'CO₂', value: '1051ppm', time: '2026-03-04 13:55:41' },
-      { id: 4, lab: '粉尘检测实验室B101(B101)', dept: '矿山安全研究所', type: 'O₂', value: '18.2%', time: '2026-03-04 13:20:10' },
-      { id: 5, lab: '有机化学实验室A103(A103)', dept: '化学品安全研究所', type: 'TVOC', value: '2.1mg/m³', time: '2026-03-04 12:45:30' },
-      { id: 6, lab: '电气安全实验室C201(C201)', dept: '安全技术研究所', type: '湿度', value: '85%', time: '2026-03-04 11:30:15' }
+      { id: 1, lab: '高温高压实验室C102(C102)', dept: '矿山安全研究所', type: '温度', value: '35.6°C', time: '2026-03-04 14:28:05', state: 'pending' },
+      { id: 2, lab: '气体分析实验室A305(A305)', dept: '职业安全研究所', type: 'CO₂', value: '1050ppm', time: '2026-03-04 13:55:42', state: 'processing' },
+      { id: 3, lab: '气体分析实验室A304(A304)', dept: '职业安全研究所', type: 'CO₂', value: '1051ppm', time: '2026-03-04 13:55:41', state: 'pending' },
+      { id: 4, lab: '粉尘检测实验室B101(B101)', dept: '矿山安全研究所', type: 'O₂', value: '18.2%', time: '2026-03-04 13:20:10', state: 'processing' },
+      { id: 5, lab: '有机化学实验室A103(A103)', dept: '化学品安全研究所', type: 'TVOC', value: '2.1mg/m³', time: '2026-03-04 12:45:30', state: 'resolved' },
+      { id: 6, lab: '电气安全实验室C201(C201)', dept: '安全技术研究所', type: '湿度', value: '85%', time: '2026-03-04 11:30:15', state: 'resolved' }
     ]
     ]
   })
   })
 }
 }
@@ -125,13 +127,13 @@ export function getDeviceStats() {
 export function getEquipmentStats() {
 export function getEquipmentStats() {
   return mockDelay({
   return mockDelay({
     categories: [
     categories: [
-      { name: '分析仪器', value: 120, color: '#2196f3' },
-      { name: '安全防护', value: 95, color: '#00e5ff' },
-      { name: '化学试剂设备', value: 80, color: '#4caf50' },
-      { name: '电气设备', value: 75, color: '#ff9800' },
-      { name: '力学测试', value: 86, color: '#f44336' },
-      { name: '环境监测', value: 72, color: '#9c27b0' },
-      { name: '通用设备', value: 58, color: '#e91e63' }
+      { name: '分析仪器', value: 120, color: '#48d7ff' },
+      { name: '安全防护', value: 95, color: '#3a7bff' },
+      { name: '化学试剂设备', value: 80, color: '#36d399' },
+      { name: '电气设备', value: 75, color: '#ffb020' },
+      { name: '力学测试', value: 86, color: '#ff8c00' },
+      { name: '环境监测', value: 72, color: '#ff4d4f' },
+      { name: '通用设备', value: 58, color: '#a78bfa' }
     ],
     ],
     summary: {
     summary: {
       totalDevices: 586,
       totalDevices: 586,
@@ -162,35 +164,55 @@ export function getBuildingTree() {
     tree: [
     tree: [
       {
       {
         id: 1,
         id: 1,
-        label: '安科院三期',
+        label: '安科院院区',
         children: [
         children: [
           {
           {
             id: 11,
             id: 11,
-            label: '主楼',
+            label: '科研楼A',
             children: [
             children: [
-              { id: 111, label: '一层', children: [
-                { id: 1111, label: '大厅' },
-                { id: 1112, label: '会议室' }
+              { id: 111, label: '1层', children: [
+                { id: 1111, label: 'A101 化学实验室' },
+                { id: 1112, label: 'A102 有机合成实验室' },
+                { id: 1113, label: 'A103 有机化学实验室' }
               ]},
               ]},
-              { id: 112, label: '层', children: [
-                { id: 1121, label: '化学分析实验室' },
-                { id: 1122, label: '有机合成实验室' }
+              { id: 112, label: '2层', children: [
+                { id: 1121, label: 'A201 材料分析实验室' },
+                { id: 1122, label: 'A202 生物安全实验室' }
               ]},
               ]},
-              { id: 113, label: '三层', children: [
-                { id: 1131, label: '微生物实验室' },
-                { id: 1132, label: '材料测试实验室' }
+              { id: 113, label: '3层', children: [
+                { id: 1131, label: 'A301 气体分析实验室' },
+                { id: 1132, label: 'A305 气体检测实验室' }
+              ]},
+              { id: 114, label: '4层', children: [
+                { id: 1141, label: 'A401 热分析实验室' }
               ]}
               ]}
             ]
             ]
           },
           },
           {
           {
             id: 12,
             id: 12,
-            label: '副楼',
+            label: '科研楼B',
+            children: [
+              { id: 121, label: '1层', children: [
+                { id: 1211, label: 'B101 粉尘检测实验室' }
+              ]},
+              { id: 122, label: '2层', children: [
+                { id: 1221, label: 'B203 材料力学实验室' },
+                { id: 1222, label: 'B205 无损检测实验室' }
+              ]}
+            ]
+          },
+          {
+            id: 13,
+            label: '科研楼C',
             children: [
             children: [
-              { id: 121, label: '一层', children: [
-                { id: 1211, label: '环境监测实验室' }
+              { id: 131, label: '1层', children: [
+                { id: 1311, label: 'C102 高温高压实验室' }
+              ]},
+              { id: 132, label: '2层', children: [
+                { id: 1321, label: 'C201 电气安全实验室' }
               ]},
               ]},
-              { id: 122, label: '二层', children: [
-                { id: 1221, label: '辐射防护实验室' }
+              { id: 133, label: '3层', children: [
+                { id: 1331, label: 'C301 防爆测试实验室' }
               ]}
               ]}
             ]
             ]
           }
           }
@@ -202,18 +224,32 @@ export function getBuildingTree() {
 
 
 // 视频列表
 // 视频列表
 export function getVideoList(params = {}) {
 export function getVideoList(params = {}) {
-  const videos = []
-  for (let i = 1; i <= 9; i++) {
-    videos.push({
-      id: i,
-      name: `监控点位 ${i}`,
-      url: '',
-      status: 'online'
-    })
-  }
+  const allVideos = [
+    { id: 1, name: 'A101 化学实验室', ai: false, status: 'online' },
+    { id: 2, name: 'A103 有机化学实验室', ai: true, status: 'online' },
+    { id: 3, name: 'A202 生物安全实验室', ai: false, status: 'online' },
+    { id: 4, name: 'A305 气体检测实验室', ai: false, status: 'online' },
+    { id: 5, name: 'A401 热分析实验室', ai: true, status: 'online' },
+    { id: 6, name: 'B101 粉尘检测实验室', ai: false, status: 'online' },
+    { id: 7, name: 'B203 材料力学实验室', ai: false, status: 'online' },
+    { id: 8, name: 'B205 无损检测实验室', ai: false, status: 'offline' },
+    { id: 9, name: 'C102 高温高压实验室', ai: true, status: 'online' },
+    { id: 10, name: 'C201 电气安全实验室', ai: false, status: 'online' },
+    { id: 11, name: 'C301 防爆测试实验室', ai: false, status: 'online' },
+    { id: 12, name: 'A102 有机合成实验室', ai: false, status: 'online' },
+    { id: 13, name: 'A201 材料分析实验室', ai: false, status: 'offline' },
+    { id: 14, name: 'A301 气体分析实验室', ai: true, status: 'online' },
+    { id: 15, name: '园区东门', ai: false, status: 'online' },
+    { id: 16, name: '园区西门', ai: false, status: 'online' },
+    { id: 17, name: '停车场', ai: false, status: 'online' },
+    { id: 18, name: '走廊通道A3F', ai: false, status: 'online' }
+  ]
+  const page = params.page || 1
+  const start = (page - 1) * 9
+  const list = allVideos.slice(start, start + 9)
   return mockDelay({
   return mockDelay({
-    total: 20,
-    page: params.page || 1,
-    list: videos
+    total: allVideos.length,
+    page,
+    list
   })
   })
 }
 }

+ 291 - 0
src/components/AlertModal.vue

@@ -0,0 +1,291 @@
+<template>
+  <transition name="alert-fade">
+    <div v-if="visible" class="alert-overlay" @click.self="close">
+      <div class="alert-modal">
+        <div class="alert-header">
+          <span class="alert-icon">&#9888;</span>
+          <span class="alert-title">安 全 预 警</span>
+        </div>
+        <div class="alert-body">
+          <div class="alert-left">
+            <div class="alert-info-row">
+              <span class="air-label">实 验 室:</span>
+              <span class="air-value bold">{{ info.lab || '-' }}</span>
+            </div>
+            <div class="alert-info-row">
+              <span class="air-label">楼栋楼层:</span>
+              <span class="air-value bold">{{ info.building || '-' }}</span>
+            </div>
+            <div class="alert-info-row">
+              <span class="air-label">所属单位:</span>
+              <span class="air-value bold">{{ info.unit || '-' }}</span>
+            </div>
+            <div class="alert-info-row">
+              <span class="air-label">异常参数:</span>
+              <span class="air-value warn">{{ info.param || '-' }}</span>
+            </div>
+            <div class="alert-info-row">
+              <span class="air-label">当前数值:</span>
+              <span class="air-value bold">{{ info.value || '-' }}</span>
+            </div>
+            <div class="alert-info-row">
+              <span class="air-label">实验室负责人:</span>
+              <span class="air-value bold">{{ info.person || '-' }}</span>
+            </div>
+            <div class="alert-info-row">
+              <span class="air-label">联系电话:</span>
+              <span class="air-value bold">{{ info.phone || '-' }}</span>
+            </div>
+            <div class="alert-info-row">
+              <span class="air-label">预警时间:</span>
+              <span class="air-value warn">{{ info.time || '-' }}</span>
+            </div>
+          </div>
+          <div class="alert-right">
+            <div class="video-label">
+              <span class="video-dot"></span>
+              <span>实时监控画面</span>
+            </div>
+            <div class="video-area">
+              <div class="video-header">
+                <span class="rec-indicator"><span class="rec-dot"></span> REC</span>
+                <span class="rec-name">{{ info.lab || '' }}</span>
+              </div>
+              <div class="video-placeholder">
+                <i class="el-icon-video-camera"></i>
+                <span>实时监控画面</span>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="alert-footer">
+          <button class="alert-btn" @click="close">确 认</button>
+        </div>
+      </div>
+    </div>
+  </transition>
+</template>
+
+<script>
+export default {
+  name: 'AlertModal',
+  data() {
+    return {
+      visible: false,
+      info: {}
+    }
+  },
+  methods: {
+    show(data) {
+      const d = data || {}
+      if (!d.time) {
+        const now = new Date()
+        const pad = n => String(n).padStart(2, '0')
+        d.time = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
+      }
+      this.info = d
+      this.visible = true
+    },
+    close() {
+      this.visible = false
+      this.$emit('close')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.alert-fade-enter-active, .alert-fade-leave-active {
+  transition: opacity 0.35s ease;
+}
+.alert-fade-enter, .alert-fade-leave-to {
+  opacity: 0;
+}
+
+.alert-overlay {
+  position: fixed;
+  inset: 0;
+  z-index: 9999;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: rgba(10, 0, 0, 0.6);
+  animation: alertOverlayGlow 2s ease-in-out infinite;
+}
+
+@keyframes alertOverlayGlow {
+  0%, 100% { box-shadow: inset 0 0 80px rgba(255, 40, 40, 0.06); }
+  50% { box-shadow: inset 0 0 140px rgba(255, 40, 40, 0.15); }
+}
+
+.alert-modal {
+  width: 750px;
+  background: rgba(18, 6, 10, 0.97);
+  border: 1px solid rgba(255, 50, 50, 0.4);
+  border-radius: 4px;
+  overflow: hidden;
+  box-shadow: 0 0 50px rgba(255, 0, 0, 0.12), 0 0 100px rgba(255, 0, 0, 0.05);
+}
+
+/* ---- Header ---- */
+.alert-header {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 14px 24px;
+  background: linear-gradient(135deg, #c41818, #a01414);
+}
+
+.alert-icon {
+  font-size: 22px;
+  color: #fff;
+}
+
+.alert-title {
+  font-size: 18px;
+  font-weight: 700;
+  color: #fff;
+  letter-spacing: 6px;
+}
+
+/* ---- Body ---- */
+.alert-body {
+  display: flex;
+  padding: 24px 28px;
+  gap: 28px;
+}
+
+.alert-left {
+  flex: 1;
+  min-width: 0;
+}
+
+.alert-right {
+  width: 280px;
+  flex-shrink: 0;
+}
+
+/* ---- Info Rows ---- */
+.alert-info-row {
+  display: flex;
+  align-items: baseline;
+  padding: 8px 0;
+}
+
+.air-label {
+  width: 112px;
+  font-size: 14px;
+  color: rgba(255, 90, 90, 0.7);
+  flex-shrink: 0;
+}
+
+.air-value {
+  font-size: 14px;
+  color: rgba(255, 255, 255, 0.9);
+
+  &.bold {
+    font-weight: 600;
+  }
+
+  &.warn {
+    color: #ff3333;
+    font-weight: 600;
+  }
+}
+
+/* ---- Video Section ---- */
+.video-label {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 13px;
+  color: rgba(255, 255, 255, 0.65);
+  margin-bottom: 10px;
+}
+
+.video-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: #ff8c00;
+  box-shadow: 0 0 6px rgba(255, 140, 0, 0.5);
+}
+
+.video-area {
+  border: 1px solid rgba(255, 50, 50, 0.35);
+  border-radius: 4px;
+  overflow: hidden;
+  background: rgba(0, 0, 0, 0.3);
+}
+
+.video-header {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px 12px;
+  font-size: 12px;
+  color: rgba(255, 255, 255, 0.55);
+}
+
+.rec-indicator {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  color: rgba(255, 255, 255, 0.55);
+}
+
+.rec-dot {
+  display: inline-block;
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: #ff4d4f;
+  animation: recBlink 1.2s ease-in-out infinite;
+}
+
+@keyframes recBlink {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0.2; }
+}
+
+.rec-name {
+  color: rgba(255, 255, 255, 0.55);
+}
+
+.video-placeholder {
+  height: 180px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  color: rgba(255, 255, 255, 0.12);
+
+  i { font-size: 40px; }
+  span { font-size: 12px; }
+}
+
+/* ---- Footer ---- */
+.alert-footer {
+  padding: 16px 28px 22px;
+  display: flex;
+  justify-content: center;
+}
+
+.alert-btn {
+  padding: 10px 52px;
+  border: 1px solid rgba(255, 50, 50, 0.5);
+  border-radius: 4px;
+  background: rgba(255, 50, 50, 0.1);
+  color: #fff;
+  font-size: 15px;
+  letter-spacing: 8px;
+  cursor: pointer;
+  transition: all 0.3s ease;
+
+  &:hover {
+    background: rgba(255, 50, 50, 0.25);
+    box-shadow: 0 0 20px rgba(255, 50, 50, 0.2);
+  }
+}
+</style>

+ 82 - 78
src/components/Header.vue

@@ -1,25 +1,19 @@
 <template>
 <template>
-  <div class="screen-header">
-    <div class="header-left">
-      <span class="header-title">中国安全生产科学研究院实验室安全智慧化管控中心</span>
+  <div class="top-nav">
+    <div class="nav-title">中国安全生产科学研究院实验室安全智慧化管控中心</div>
+    <div class="nav-tabs">
+      <div
+        v-for="tab in tabs"
+        :key="tab.path"
+        class="nav-tab"
+        :class="{ active: currentPath === tab.path }"
+        @click="handleTabClick(tab)"
+      >{{ tab.label }}</div>
     </div>
     </div>
-    <div class="header-center">
-      <div class="nav-tabs">
-        <div
-          v-for="tab in tabs"
-          :key="tab.path"
-          class="nav-tab"
-          :class="{ active: currentPath === tab.path }"
-          @click="handleTabClick(tab)"
-        >
-          {{ tab.label }}
-        </div>
-      </div>
-    </div>
-    <div class="header-right">
-      <span class="header-weather">{{ weather }}</span>
-      <span class="header-date">{{ dateStr }}</span>
-      <span class="header-time">{{ timeStr }}</span>
+    <div class="nav-right">
+      <span class="weather">{{ weather }}</span>
+      <span class="weekday">{{ dateStr }}</span>
+      <span class="clock">{{ timeStr }}</span>
     </div>
     </div>
   </div>
   </div>
 </template>
 </template>
@@ -37,7 +31,7 @@ export default {
       ],
       ],
       dateStr: '',
       dateStr: '',
       timeStr: '',
       timeStr: '',
-      weather: '北京·晴 18°C'
+      weather: '北京 · 晴 18°C'
     }
     }
   },
   },
   computed: {
   computed: {
@@ -58,7 +52,7 @@ export default {
       const now = new Date()
       const now = new Date()
       const pad = n => String(n).padStart(2, '0')
       const pad = n => String(n).padStart(2, '0')
       const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
       const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
-      this.dateStr = `${weekDays[now.getDay()]}`
+      this.dateStr = weekDays[now.getDay()]
       this.timeStr = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
       this.timeStr = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
     },
     },
     handleTabClick(tab) {
     handleTabClick(tab) {
@@ -66,14 +60,13 @@ export default {
         this.$router.push(tab.path)
         this.$router.push(tab.path)
       }
       }
     },
     },
-    // 天气数据 - 后续由后台对接真实接口
     async fetchWeather() {
     async fetchWeather() {
       try {
       try {
         const res = await getWeather()
         const res = await getWeather()
         const d = res.data
         const d = res.data
-        this.weather = `${d.city}·${d.weather} ${d.temp}°C`
+        this.weather = `${d.city} · ${d.weather} ${d.temp}°C`
       } catch (e) {
       } catch (e) {
-        this.weather = '北京·晴 18°C'
+        this.weather = '北京 · 晴 18°C'
       }
       }
     }
     }
   }
   }
@@ -81,106 +74,117 @@ export default {
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
-.screen-header {
+.top-nav {
   width: 100%;
   width: 100%;
   height: $header-height;
   height: $header-height;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: space-between;
   justify-content: space-between;
   padding: 0 30px;
   padding: 0 30px;
-  background: linear-gradient(180deg, rgba(10, 26, 58, 0.95) 0%, rgba(10, 26, 58, 0.75) 100%);
-  border-bottom: 1px solid rgba(30, 144, 255, 0.15);
+  background: linear-gradient(180deg, rgba(10,30,70,0.95) 0%, rgba(6,20,50,0.75) 100%);
+  border-bottom: 1px solid $border-color;
   position: relative;
   position: relative;
-  z-index: 10;
+  z-index: 100;
 
 
+  // 底部静态渐变线
   &::after {
   &::after {
     content: '';
     content: '';
     position: absolute;
     position: absolute;
     bottom: 0;
     bottom: 0;
+    left: 10%;
+    right: 10%;
+    height: 1px;
+    background: linear-gradient(90deg, transparent, $accent, transparent);
+  }
+
+  // 底部流光
+  &::before {
+    content: '';
+    position: absolute;
+    bottom: -1px;
     left: 0;
     left: 0;
-    width: 100%;
+    width: 120px;
     height: 2px;
     height: 2px;
-    background: linear-gradient(90deg, transparent 0%, #3BAFD6 50%, transparent 100%);
-    background-size: 200% 100%;
-    animation: headerGlow 3s linear infinite;
+    background: linear-gradient(90deg, transparent, $accent, rgba(72,215,255,0.8), transparent);
+    animation: navFlow 4s linear infinite;
+    z-index: 101;
   }
   }
 }
 }
 
 
-@keyframes headerGlow {
-  0% { background-position: 100% 0; }
-  100% { background-position: -100% 0; }
-}
-
-.header-left {
-  flex: 1;
+@keyframes navFlow {
+  0% { left: -120px; }
+  100% { left: calc(100% + 120px); }
 }
 }
 
 
-.header-title {
-  font-size: 24px;
-  font-weight: bold;
-  color: #fff;
-  letter-spacing: 4px;
-  text-shadow: 0 0 15px rgba(30, 144, 255, 0.4);
+.nav-title {
+  font-size: 26px;
+  font-weight: 700;
+  letter-spacing: 6px;
+  background: linear-gradient(135deg, #fff 0%, $accent 100%);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  text-shadow: 0 0 30px rgba(72,215,255,0.3);
+  animation: titleGlow 3s ease-in-out infinite;
 }
 }
 
 
-.header-center {
-  flex: 0 0 auto;
+@keyframes titleGlow {
+  0%, 100% { filter: drop-shadow(0 0 6px rgba(72,215,255,0.2)); }
+  50% { filter: drop-shadow(0 0 16px rgba(72,215,255,0.5)); }
 }
 }
 
 
 .nav-tabs {
 .nav-tabs {
   display: flex;
   display: flex;
   gap: 4px;
   gap: 4px;
-  background: rgba(13, 34, 71, 0.8);
-  border: 1px solid rgba(30, 144, 255, 0.3);
-  border-radius: 4px;
-  padding: 4px;
 }
 }
 
 
 .nav-tab {
 .nav-tab {
-  padding: 8px 24px;
-  font-size: 14px;
-  color: $text-secondary;
+  padding: 8px 28px;
+  border-radius: 4px;
   cursor: pointer;
   cursor: pointer;
-  border-radius: 3px;
+  font-size: 15px;
+  letter-spacing: 2px;
   transition: all 0.3s;
   transition: all 0.3s;
-  white-space: nowrap;
+  border: 1px solid transparent;
+  color: $text-secondary;
+  background: transparent;
 
 
   &:hover {
   &:hover {
-    color: #fff;
-    background: rgba(30, 144, 255, 0.15);
+    color: $accent;
+    border-color: $border-color;
   }
   }
 
 
   &.active {
   &.active {
+    background: linear-gradient(135deg, rgba(72,215,255,0.18), rgba(58,123,255,0.12));
+    border-color: $accent;
     color: #fff;
     color: #fff;
-    background: linear-gradient(135deg, #1565c0, #1e88e5);
-    box-shadow: 0 0 10px rgba(30, 144, 255, 0.3);
+    box-shadow: 0 0 16px rgba(72,215,255,0.2);
+    animation: tabGlow 2.5s ease-in-out infinite;
   }
   }
 }
 }
 
 
-.header-right {
-  flex: 1;
+@keyframes tabGlow {
+  0%, 100% { box-shadow: 0 0 10px rgba(72,215,255,0.15); }
+  50% { box-shadow: 0 0 22px rgba(72,215,255,0.35), inset 0 0 10px rgba(72,215,255,0.08); }
+}
+
+.nav-right {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
-  justify-content: flex-end;
   gap: 20px;
   gap: 20px;
-}
-
-.header-weather {
   font-size: 14px;
   font-size: 14px;
   color: $text-secondary;
   color: $text-secondary;
-}
 
 
-.header-date {
-  font-size: 14px;
-  color: $text-secondary;
+  .clock {
+    font-size: 22px;
+    font-weight: 600;
+    color: $accent;
+    letter-spacing: 2px;
+    animation: clockPulse 2s ease-in-out infinite;
+  }
 }
 }
 
 
-.header-time {
-  font-size: 22px;
-  color: $accent-cyan;
-  font-family: 'Courier New', monospace;
-  letter-spacing: 2px;
-  font-weight: bold;
-  text-shadow: 0 0 8px rgba(0, 229, 255, 0.3);
+@keyframes clockPulse {
+  0%, 100% { text-shadow: 0 0 6px rgba(72,215,255,0.3); }
+  50% { text-shadow: 0 0 14px rgba(72,215,255,0.6), 0 0 28px rgba(72,215,255,0.2); }
 }
 }
 </style>
 </style>

+ 71 - 115
src/components/LabStats/BasicStats.vue

@@ -1,28 +1,22 @@
 <template>
 <template>
   <div class="panel-box basic-stats">
   <div class="panel-box basic-stats">
+    <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
+    <div class="corner-deco bl"></div><div class="corner-deco br"></div>
     <div class="panel-title">实验室基本情况统计</div>
     <div class="panel-title">实验室基本情况统计</div>
     <div class="panel-content">
     <div class="panel-content">
-      <div class="chart-row">
-        <div ref="chart" class="chart-area"></div>
-        <div class="legend-list">
-          <div class="legend-item" v-for="item in levels" :key="item.name">
-            <span class="legend-dot" :style="{ background: item.color }"></span>
-            <span class="legend-label">{{ item.name }}</span>
-          </div>
+      <div ref="chart" class="chart-area"></div>
+      <div class="status-cards">
+        <div class="status-card using">
+          <div class="label">使用</div>
+          <div class="value">{{ usage.using }}<span class="unit"> 间</span></div>
         </div>
         </div>
-      </div>
-      <div class="usage-row">
-        <div class="usage-item">
-          <div class="usage-label">使用</div>
-          <div class="usage-value c-using">{{ usage.using }}<span class="usage-unit">间</span></div>
-        </div>
-        <div class="usage-item">
-          <div class="usage-label">异常</div>
-          <div class="usage-value c-abnormal">{{ usage.abnormal }}<span class="usage-unit">间</span></div>
+        <div class="status-card error">
+          <div class="label">异常</div>
+          <div class="value">{{ usage.abnormal }}<span class="unit"> 间</span></div>
         </div>
         </div>
-        <div class="usage-item">
-          <div class="usage-label">空闲</div>
-          <div class="usage-value c-idle">{{ usage.idle }}<span class="usage-unit">间</span></div>
+        <div class="status-card idle">
+          <div class="label">空闲</div>
+          <div class="value">{{ usage.idle }}<span class="unit"> 间</span></div>
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
@@ -57,41 +51,31 @@ export default {
       const option = {
       const option = {
         tooltip: {
         tooltip: {
           trigger: 'item',
           trigger: 'item',
-          backgroundColor: 'rgba(13,34,71,0.9)',
-          borderColor: '#1a3a6a',
-          textStyle: { color: '#fff', fontSize: 12 }
+          backgroundColor: 'rgba(6,22,56,0.9)',
+          borderColor: 'rgba(72,180,255,0.3)',
+          textStyle: { color: '#d8f4ff' }
+        },
+        legend: {
+          orient: 'vertical',
+          right: 10,
+          top: 'center',
+          textStyle: { color: '#7eacc8', fontSize: 11 },
+          itemWidth: 10,
+          itemHeight: 10
         },
         },
         series: [{
         series: [{
           type: 'pie',
           type: 'pie',
-          radius: ['55%', '78%'],
-          center: ['50%', '50%'],
-          avoidLabelOverlap: false,
+          radius: ['45%', '70%'],
+          center: ['35%', '50%'],
           label: {
           label: {
             show: true,
             show: true,
             position: 'center',
             position: 'center',
-            formatter: [
-              '{num|' + data.totalLabs + '}',
-              '{txt|实验室总数}'
-            ].join('\n'),
-            rich: {
-              num: { fontSize: 28, fontWeight: 'bold', color: '#fff', fontFamily: 'Courier New', lineHeight: 36 },
-              txt: { fontSize: 12, color: '#8cb8e6', lineHeight: 20 }
-            }
-          },
-          emphasis: {
-            label: {
-              show: true,
-              formatter: [
-                '{num|' + data.totalLabs + '}',
-                '{txt|实验室总数}'
-              ].join('\n'),
-              rich: {
-                num: { fontSize: 28, fontWeight: 'bold', color: '#fff', fontFamily: 'Courier New', lineHeight: 36 },
-                txt: { fontSize: 12, color: '#8cb8e6', lineHeight: 20 }
-              }
-            }
+            formatter: data.totalLabs + '\n实验室总数',
+            fontSize: 14,
+            color: '#fff',
+            lineHeight: 20
           },
           },
-          labelLine: { show: false },
+          emphasis: { scaleSize: 6 },
           data: data.levels.map(l => ({
           data: data.levels.map(l => ({
             name: l.name,
             name: l.name,
             value: l.value,
             value: l.value,
@@ -107,94 +91,66 @@ export default {
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
 .basic-stats {
 .basic-stats {
-  height: 100%;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
-
-  .panel-content {
-    flex: 1;
-    display: flex;
-    flex-direction: column;
-    overflow: hidden;
-  }
-}
-
-.chart-row {
-  flex: 1;
-  display: flex;
-  align-items: center;
-  min-height: 0;
 }
 }
 
 
 .chart-area {
 .chart-area {
   flex: 1;
   flex: 1;
-  height: 100%;
+  width: 100%;
   min-height: 0;
   min-height: 0;
 }
 }
 
 
-.legend-list {
-  width: 90px;
-  flex-shrink: 0;
-  display: flex;
-  flex-direction: column;
-  gap: 10px;
-}
-
-.legend-item {
+.status-cards {
   display: flex;
   display: flex;
-  align-items: center;
-  gap: 6px;
-}
-
-.legend-dot {
-  width: 10px;
-  height: 10px;
-  border-radius: 2px;
-  flex-shrink: 0;
-}
-
-.legend-label {
-  font-size: 12px;
-  color: $text-secondary;
-  white-space: nowrap;
-}
-
-.usage-row {
-  display: flex;
-  gap: 8px;
+  gap: 12px;
   flex-shrink: 0;
   flex-shrink: 0;
-  margin-top: 6px;
 }
 }
 
 
-.usage-item {
+.status-card {
   flex: 1;
   flex: 1;
   text-align: center;
   text-align: center;
-  padding: 8px 0;
-  background: rgba(255, 255, 255, 0.03);
-  border: 1px solid rgba(255, 255, 255, 0.05);
+  padding: 6px;
   border-radius: 4px;
   border-radius: 4px;
-}
+  background: rgba(72,215,255,0.06);
+  border: 1px solid rgba(72,215,255,0.12);
+  transition: all 0.3s ease;
+  animation: cardBreathe 4s ease-in-out infinite;
+
+  &:nth-child(2) { animation-delay: 1.3s; }
+  &:nth-child(3) { animation-delay: 2.6s; }
+
+  &:hover {
+    transform: translateY(-2px);
+    border-color: $accent !important;
+    box-shadow: 0 4px 16px rgba(72,215,255,0.15) !important;
+  }
 
 
-.usage-label {
-  font-size: 12px;
-  color: $text-muted;
-  margin-bottom: 4px;
-}
+  .label {
+    font-size: 12px;
+    color: $text-secondary;
+    margin-bottom: 2px;
+  }
 
 
-.usage-value {
-  font-size: 22px;
-  font-weight: bold;
-  font-family: 'Courier New', monospace;
+  .value {
+    font-size: 22px;
+    font-weight: 700;
+    font-family: 'Courier New', monospace;
+  }
+
+  .unit {
+    font-size: 12px;
+    color: $text-secondary;
+    font-weight: normal;
+  }
 
 
-  &.c-using { color: #3EBCE3; }
-  &.c-abnormal { color: #EE4A4E; }
-  &.c-idle { color: #33C793; }
+  &.using .value { color: $accent; }
+  &.error .value { color: $accent-red; }
+  &.idle .value { color: $accent-green; }
 }
 }
 
 
-.usage-unit {
-  font-size: 12px;
-  color: $text-muted;
-  font-weight: normal;
-  margin-left: 2px;
+@keyframes cardBreathe {
+  0%, 100% { border-color: rgba(72,215,255,0.12); box-shadow: 0 0 0 transparent; }
+  50% { border-color: rgba(72,215,255,0.3); box-shadow: 0 0 10px rgba(72,215,255,0.08); }
 }
 }
 </style>
 </style>

+ 86 - 98
src/components/LabStats/DeviceStats.vue

@@ -1,23 +1,25 @@
 <template>
 <template>
   <div class="panel-box device-stats">
   <div class="panel-box device-stats">
+    <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
+    <div class="corner-deco bl"></div><div class="corner-deco br"></div>
     <div class="panel-title">智能环境感知应用设备统计</div>
     <div class="panel-title">智能环境感知应用设备统计</div>
     <div class="panel-content">
     <div class="panel-content">
-      <div class="status-row">
-        <div class="status-box">
-          <div class="status-label">在线设备</div>
-          <div class="status-value">{{ online }}</div>
+      <div class="device-top">
+        <div class="device-stat-card">
+          <div class="ds-label">在线设备</div>
+          <div class="ds-value">{{ online }}</div>
         </div>
         </div>
-        <div class="status-box">
-          <div class="status-label">离线设备</div>
-          <div class="status-value offline">{{ offline }}</div>
+        <div class="device-stat-card offline">
+          <div class="ds-label">离线设备</div>
+          <div class="ds-value">{{ offline }}</div>
         </div>
         </div>
       </div>
       </div>
-      <div class="main-row">
-        <div ref="chart" class="gauge-area"></div>
-        <div class="device-list">
-          <div class="device-item" v-for="item in devices" :key="item.name">
-            <span class="device-name">{{ item.name }}</span>
-            <span class="device-count">{{ item.count }}</span>
+      <div class="device-bottom">
+        <div ref="chart" class="device-gauge-wrap"></div>
+        <div class="device-type-list">
+          <div class="device-type-item" v-for="item in devices" :key="item.name">
+            <span class="dt-name">{{ item.name }}</span>
+            <span class="dt-value">{{ item.count }}</span>
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
@@ -32,12 +34,7 @@ import { getDeviceStats } from '@/api'
 export default {
 export default {
   name: 'DeviceStats',
   name: 'DeviceStats',
   data() {
   data() {
-    return {
-      online: 0,
-      offline: 0,
-      devices: [],
-      chart: null
-    }
+    return { online: 0, offline: 0, devices: [], chart: null }
   },
   },
   async mounted() {
   async mounted() {
     const res = await getDeviceStats()
     const res = await getDeviceStats()
@@ -55,45 +52,38 @@ export default {
       const option = {
       const option = {
         series: [{
         series: [{
           type: 'gauge',
           type: 'gauge',
-          startAngle: 225,
-          endAngle: -45,
-          radius: '88%',
-          center: ['50%', '55%'],
+          startAngle: 210,
+          endAngle: -30,
+          radius: '90%',
+          center: ['50%', '58%'],
           min: 0,
           min: 0,
           max: 100,
           max: 100,
-          progress: {
-            show: true,
-            width: 14,
-            itemStyle: {
-              color: {
-                type: 'linear',
-                x: 0, y: 1, x2: 0, y2: 0,
-                colorStops: [
-                  { offset: 0, color: '#144366' },
-                  { offset: 0.5, color: '#226D8E' },
-                  { offset: 1, color: '#47D7FF' }
-                ]
-              }
-            }
-          },
           axisLine: {
           axisLine: {
-            lineStyle: { width: 14, color: [[1, 'rgba(255,255,255,0.06)']] }
+            lineStyle: {
+              width: 14,
+              color: [[0.7,'rgba(72,215,255,0.2)'],[0.9,'rgba(72,215,255,0.4)'],[1,'#48d7ff']]
+            }
           },
           },
           axisTick: { show: false },
           axisTick: { show: false },
           splitLine: { show: false },
           splitLine: { show: false },
           axisLabel: { show: false },
           axisLabel: { show: false },
-          pointer: { show: false },
+          pointer: {
+            length: '60%',
+            width: 4,
+            itemStyle: { color: '#48d7ff' }
+          },
           title: {
           title: {
             show: true,
             show: true,
-            offsetCenter: [0, '30%'],
+            offsetCenter: [0, '72%'],
             fontSize: 12,
             fontSize: 12,
-            color: '#8cb8e6'
+            color: '#7eacc8'
           },
           },
           detail: {
           detail: {
-            fontSize: 28,
-            fontWeight: 'bold',
-            color: '#fff',
-            offsetCenter: [0, '-5%'],
+            valueAnimation: true,
+            fontSize: 26,
+            color: '#48d7ff',
+            fontWeight: 700,
+            offsetCenter: [0, '40%'],
             formatter: '{value}%',
             formatter: '{value}%',
             fontFamily: 'Courier New'
             fontFamily: 'Courier New'
           },
           },
@@ -108,90 +98,88 @@ export default {
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
 .device-stats {
 .device-stats {
-  height: 100%;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
-
-  .panel-content {
-    flex: 1;
-    display: flex;
-    flex-direction: column;
-    overflow: hidden;
-  }
 }
 }
 
 
-.status-row {
+.device-top {
   display: flex;
   display: flex;
-  gap: 10px;
+  gap: 12px;
+  margin-bottom: 10px;
   flex-shrink: 0;
   flex-shrink: 0;
-  margin-bottom: 6px;
 }
 }
 
 
-.status-box {
+.device-stat-card {
   flex: 1;
   flex: 1;
   text-align: center;
   text-align: center;
-  padding: 10px 0;
-  background: rgba(255, 255, 255, 0.03);
-  border: 1px solid rgba(255, 255, 255, 0.05);
+  padding: 8px;
   border-radius: 4px;
   border-radius: 4px;
-}
+  background: rgba(72,215,255,0.06);
+  border: 1px solid rgba(72,215,255,0.1);
+  transition: all 0.3s ease;
 
 
-.status-label {
-  font-size: 12px;
-  color: #7EACC8;
-  margin-bottom: 4px;
-}
-
-.status-value {
-  font-size: 28px;
-  font-weight: bold;
-  color: $accent-cyan;
-  font-family: 'Courier New', monospace;
+  &:hover {
+    border-color: rgba(72,215,255,0.3);
+    box-shadow: 0 0 12px rgba(72,215,255,0.1);
+    transform: translateY(-2px);
+  }
 
 
-  &.offline {
-    color: #7EACC8;
+  .ds-label { font-size: 12px; color: #7EACC8; }
+  .ds-value {
+    font-size: 22px;
+    font-weight: 700;
+    color: $accent;
+    font-family: 'Courier New', monospace;
   }
   }
+
+  &.offline .ds-value { color: $text-secondary; }
 }
 }
 
 
-.main-row {
-  flex: 1;
+.device-bottom {
   display: flex;
   display: flex;
+  gap: 10px;
+  align-items: center;
+  flex: 1;
   min-height: 0;
   min-height: 0;
 }
 }
 
 
-.gauge-area {
-  flex: 1;
-  min-height: 0;
+.device-gauge-wrap {
+  width: 50%;
+  height: 100%;
+  min-height: 140px;
 }
 }
 
 
-.device-list {
-  width: 140px;
-  flex-shrink: 0;
+.device-type-list {
+  flex: 1;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
+  gap: 10px;
   justify-content: center;
   justify-content: center;
-  gap: 8px;
 }
 }
 
 
-.device-item {
+.device-type-item {
   display: flex;
   display: flex;
   justify-content: space-between;
   justify-content: space-between;
   align-items: center;
   align-items: center;
-  padding: 8px 10px;
-  background: rgba(255, 255, 255, 0.03);
-  border: 1px solid rgba(255, 255, 255, 0.05);
+  padding: 10px 14px;
   border-radius: 4px;
   border-radius: 4px;
-}
+  background: rgba(72,215,255,0.04);
+  border: 1px solid rgba(72,215,255,0.08);
+  transition: all 0.3s ease;
 
 
-.device-name {
-  font-size: 12px;
-  color: $text-secondary;
-}
+  &:hover {
+    border-color: rgba(72,215,255,0.35);
+    background: rgba(72,215,255,0.08);
+    box-shadow: 0 0 12px rgba(72,215,255,0.1);
+    transform: translateX(3px);
+  }
 
 
-.device-count {
-  font-size: 18px;
-  font-weight: bold;
-  color: $accent-cyan;
-  font-family: 'Courier New', monospace;
+  .dt-name { font-size: 13px; color: $text-secondary; }
+  .dt-value {
+    font-size: 18px;
+    font-weight: 700;
+    color: $accent;
+    font-family: 'Courier New', monospace;
+  }
 }
 }
 </style>
 </style>

+ 84 - 110
src/components/LabStats/EnvSensing.vue

@@ -1,46 +1,23 @@
 <template>
 <template>
   <div class="panel-box env-sensing">
   <div class="panel-box env-sensing">
+    <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
+    <div class="corner-deco bl"></div><div class="corner-deco br"></div>
     <div class="panel-title">实验环境安全智能感知</div>
     <div class="panel-title">实验环境安全智能感知</div>
     <div class="panel-content">
     <div class="panel-content">
-      <div class="list-wrapper" ref="listWrapper" @mouseenter="pauseScroll" @mouseleave="resumeScroll">
-        <div ref="scrollContent" class="scroll-content">
-          <div ref="listInner" class="list-inner">
-            <div class="env-item" v-for="(item, idx) in list" :key="'a'+idx">
-              <div class="env-header">
-                <span class="env-lab">{{ item.lab }}</span>
-                <span class="env-dept">{{ item.dept }}</span>
-              </div>
-              <div class="env-data">
-                <span class="data-tag normal">温度:{{ item.temp }}°C</span>
-                <span class="data-tag normal">湿度:{{ item.humidity }}%</span>
-                <span class="data-tag normal">TVOC {{ item.tvoc }}mg/m³</span>
-                <span class="data-tag" :class="item.co2.warn ? 'warn' : 'normal'">
-                  <template v-if="item.co2.warn">△</template>CO₂ {{ item.co2.value }}ppm
-                </span>
-                <span class="data-tag" :class="item.o2.warn ? 'warn' : 'normal'">
-                  <template v-if="item.o2.warn">△</template>O₂ {{ item.o2.value }}%
-                </span>
-              </div>
-            </div>
-          </div>
-          <!-- 复制一份用于无缝滚动 -->
-          <div class="list-inner" v-if="needScroll">
-            <div class="env-item" v-for="(item, idx) in list" :key="'b'+idx">
-              <div class="env-header">
-                <span class="env-lab">{{ item.lab }}</span>
-                <span class="env-dept">{{ item.dept }}</span>
-              </div>
-              <div class="env-data">
-                <span class="data-tag normal">温度:{{ item.temp }}°C</span>
-                <span class="data-tag normal">湿度:{{ item.humidity }}%</span>
-                <span class="data-tag normal">TVOC {{ item.tvoc }}mg/m³</span>
-                <span class="data-tag" :class="item.co2.warn ? 'warn' : 'normal'">
-                  <template v-if="item.co2.warn">△</template>CO₂ {{ item.co2.value }}ppm
-                </span>
-                <span class="data-tag" :class="item.o2.warn ? 'warn' : 'normal'">
-                  <template v-if="item.o2.warn">△</template>O₂ {{ item.o2.value }}%
-                </span>
-              </div>
+      <div class="sensor-scroll-wrap" ref="scrollWrap" @mouseenter="paused=true" @mouseleave="paused=false">
+        <div ref="scrollInner" class="sensor-scroll-inner">
+          <div class="sensor-item" v-for="(item, idx) in displayList" :key="idx">
+            <div class="lab-name">{{ item.lab }}<span class="lab-unit">{{ item.dept }}</span></div>
+            <div class="sensor-values">
+              <span class="sensor-val">温度:{{ item.temp }}°C</span>
+              <span class="sensor-val">湿度:{{ item.humidity }}%</span>
+              <span class="sensor-val">TVOC {{ item.tvoc }}mg/m³</span>
+              <span class="sensor-val" :class="{ alarm: item.co2.warn }">
+                <span v-if="item.co2.warn" class="alarm-icon">⚠</span>CO₂ {{ item.co2.value }}ppm
+              </span>
+              <span class="sensor-val" :class="{ alarm: item.o2.warn }">
+                <span v-if="item.o2.warn" class="alarm-icon">⚠</span>O₂ {{ item.o2.value }}%
+              </span>
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
@@ -57,132 +34,129 @@ export default {
   data() {
   data() {
     return {
     return {
       list: [],
       list: [],
-      needScroll: false,
-      scrollTimer: null,
       paused: false,
       paused: false,
+      scrollTimer: null,
       scrollY: 0
       scrollY: 0
     }
     }
   },
   },
+  computed: {
+    // 复制一份数据实现无缝滚动
+    displayList() {
+      return [...this.list, ...this.list]
+    }
+  },
   async mounted() {
   async mounted() {
     const res = await getEnvSensingList()
     const res = await getEnvSensingList()
     this.list = res.data.list
     this.list = res.data.list
     this.$nextTick(() => {
     this.$nextTick(() => {
-      // 延迟检测,确保DOM完全渲染和布局完成
-      setTimeout(() => this.checkScroll(), 300)
+      setTimeout(() => this.startScroll(), 500)
     })
     })
   },
   },
   beforeDestroy() {
   beforeDestroy() {
     if (this.scrollTimer) cancelAnimationFrame(this.scrollTimer)
     if (this.scrollTimer) cancelAnimationFrame(this.scrollTimer)
   },
   },
   methods: {
   methods: {
-    checkScroll() {
-      const wrapper = this.$refs.listWrapper
-      const inner = this.$refs.listInner
-      if (!wrapper || !inner) return
-      // 内容高度超过容器时开启无缝滚动
-      this.needScroll = inner.offsetHeight > wrapper.offsetHeight
-      if (this.needScroll) {
-        this.$nextTick(() => {
-          setTimeout(() => this.startScroll(), 100)
-        })
-      }
-    },
     startScroll() {
     startScroll() {
-      const wrapper = this.$refs.listWrapper
-      const inner = this.$refs.listInner
-      const scrollContent = this.$refs.scrollContent
-      if (!wrapper || !inner || !scrollContent) return
-      const firstHeight = inner.offsetHeight
-      const speed = 0.4 // px per frame (~24px/s at 60fps)
+      const wrap = this.$refs.scrollWrap
+      const inner = this.$refs.scrollInner
+      if (!wrap || !inner) return
+      // 一半高度即为一份数据的高度
+      const halfHeight = inner.scrollHeight / 2
+      if (halfHeight <= wrap.offsetHeight) return // 不需要滚动
+
+      const speed = 0.4
       this.scrollY = 0
       this.scrollY = 0
 
 
       const step = () => {
       const step = () => {
         if (!this.paused) {
         if (!this.paused) {
           this.scrollY += speed
           this.scrollY += speed
-          // 当滚动到第一份内容末尾时,无缝回到顶部
-          if (this.scrollY >= firstHeight) {
-            this.scrollY -= firstHeight
+          if (this.scrollY >= halfHeight) {
+            this.scrollY -= halfHeight
           }
           }
-          scrollContent.style.transform = `translateY(${-this.scrollY}px)`
+          inner.style.transform = `translateY(${-this.scrollY}px)`
         }
         }
         this.scrollTimer = requestAnimationFrame(step)
         this.scrollTimer = requestAnimationFrame(step)
       }
       }
       this.scrollTimer = requestAnimationFrame(step)
       this.scrollTimer = requestAnimationFrame(step)
-    },
-    pauseScroll() { this.paused = true },
-    resumeScroll() { this.paused = false }
+    }
   }
   }
 }
 }
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
 .env-sensing {
 .env-sensing {
-  height: 100%;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
-
-  .panel-content {
-    flex: 1;
-    overflow: hidden;
-    padding: 0;
-  }
 }
 }
 
 
-.list-wrapper {
-  height: 100%;
+.sensor-scroll-wrap {
+  flex: 1;
   overflow: hidden;
   overflow: hidden;
-  padding: 0 14px;
+  position: relative;
+  min-height: 0;
 }
 }
 
 
-.env-item {
-  padding: 10px 0;
-  border-bottom: 1px solid rgba(26, 58, 106, 0.3);
-}
-
-.env-header {
-  display: flex;
-  align-items: baseline;
-  gap: 10px;
+.sensor-item {
+  padding: 10px 12px;
   margin-bottom: 6px;
   margin-bottom: 6px;
+  border-radius: 4px;
+  background: rgba(72,215,255,0.04);
+  border: 1px solid rgba(72,215,255,0.08);
+  transition: all 0.3s;
+
+  &:hover {
+    border-color: $accent;
+    background: rgba(72,215,255,0.08);
+  }
 }
 }
 
 
-.env-lab {
+.lab-name {
   font-size: 13px;
   font-size: 13px;
-  font-weight: bold;
+  font-weight: 600;
+  margin-bottom: 6px;
   color: #fff;
   color: #fff;
 }
 }
 
 
-.env-dept {
+.lab-unit {
   font-size: 11px;
   font-size: 11px;
-  color: $text-muted;
+  color: $text-secondary;
+  margin-left: 6px;
 }
 }
 
 
-.env-data {
+.sensor-values {
   display: flex;
   display: flex;
+  gap: 10px;
   flex-wrap: wrap;
   flex-wrap: wrap;
-  gap: 6px 10px;
 }
 }
 
 
-.data-tag {
+.sensor-val {
   font-size: 12px;
   font-size: 12px;
-  color: $text-secondary;
+  padding: 2px 8px;
+  border-radius: 3px;
+  background: rgba(72,215,255,0.06);
+  color: $text-primary;
 
 
-  &.normal {
-    background: rgba(15, 45, 77, 0.5);
-    border: 1px solid rgba(15, 45, 77, 0.8);
-    box-shadow: 0 0 4px rgba(15, 45, 77, 0.4);
-    border-radius: 3px;
-    padding: 1px 6px;
+  &.alarm {
+    background: rgba(255,77,79,0.15);
+    color: $accent-red;
+    font-weight: 600;
+    animation: alarmPulse 1s ease-in-out infinite alternate;
   }
   }
+}
 
 
-  &.warn {
-    color: #f44336;
-    font-weight: bold;
-    background: rgba(244, 67, 54, 0.08);
-    border: 1px solid rgba(244, 67, 54, 0.35);
-    box-shadow: 0 0 6px rgba(244, 67, 54, 0.2);
-    border-radius: 3px;
-    padding: 1px 6px;
-  }
+.alarm-icon {
+  display: inline-block;
+  margin-right: 2px;
+  animation: alarmIconShake 0.5s ease-in-out infinite alternate;
+}
+
+@keyframes alarmPulse {
+  0% { box-shadow: 0 0 4px rgba(255,77,79,0.3); }
+  100% { box-shadow: 0 0 12px rgba(255,77,79,0.6); }
+}
+
+@keyframes alarmIconShake {
+  0% { transform: translateX(-1px) rotate(-5deg); }
+  100% { transform: translateX(1px) rotate(5deg); }
 }
 }
 </style>
 </style>

+ 88 - 90
src/components/LabStats/EquipmentStats.vue

@@ -1,39 +1,29 @@
 <template>
 <template>
   <div class="panel-box equipment-stats">
   <div class="panel-box equipment-stats">
+    <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
+    <div class="corner-deco bl"></div><div class="corner-deco br"></div>
     <div class="panel-title">实验室设备分类及使用统计</div>
     <div class="panel-title">实验室设备分类及使用统计</div>
     <div class="panel-content">
     <div class="panel-content">
-      <div ref="chart" class="chart-area"></div>
-      <div class="stats-row top-stats">
-        <div class="stat-item">
-          <div class="stat-value c-main">{{ summary.totalDevices }}</div>
-          <div class="stat-label">设备总数</div>
+      <div ref="chart" class="equip-chart-wrap"></div>
+      <div class="equip-mid">
+        <div class="equip-mid-item">
+          <div class="em-value">{{ summary.totalDevices }}</div>
+          <div class="em-label">设备总数</div>
         </div>
         </div>
-        <div class="stat-item">
-          <div class="stat-value c-main">{{ summary.totalHours }}<span class="stat-unit">h</span></div>
-          <div class="stat-label">使用总时长</div>
+        <div class="equip-mid-item">
+          <div class="em-value">{{ summary.totalHours }}<span class="em-unit">h</span></div>
+          <div class="em-label">使用总时长</div>
         </div>
         </div>
-        <div class="stat-item">
-          <div class="stat-value c-main">{{ summary.usageRate }}<span class="stat-unit">%</span></div>
-          <div class="stat-label">设备使用率</div>
+        <div class="equip-mid-item">
+          <div class="em-value">{{ summary.usageRate }}<span class="em-unit">%</span></div>
+          <div class="em-label">设备使用率</div>
         </div>
         </div>
       </div>
       </div>
-      <div class="stats-row bottom-stats">
-        <div class="stat-item">
-          <div class="stat-value c-main">{{ status.using }}</div>
-          <div class="stat-label">使用</div>
-        </div>
-        <div class="stat-item">
-          <div class="stat-value c-idle">{{ status.idle }}</div>
-          <div class="stat-label">空闲</div>
-        </div>
-        <div class="stat-item">
-          <div class="stat-value c-normal">{{ status.normal }}</div>
-          <div class="stat-label">正常</div>
-        </div>
-        <div class="stat-item">
-          <div class="stat-value c-repair">{{ status.repair }}</div>
-          <div class="stat-label">维修</div>
-        </div>
+      <div class="equip-bottom">
+        <div class="equip-status-card"><div class="es-value" style="color:#48d7ff">{{ status.using }}</div><div class="es-label">使用</div></div>
+        <div class="equip-status-card"><div class="es-value" style="color:#36d399">{{ status.idle }}</div><div class="es-label">空闲</div></div>
+        <div class="equip-status-card"><div class="es-value" style="color:#ffb020">{{ status.normal }}</div><div class="es-label">正常</div></div>
+        <div class="equip-status-card"><div class="es-value" style="color:#ff4d4f">{{ status.repair }}</div><div class="es-label">维修</div></div>
       </div>
       </div>
     </div>
     </div>
   </div>
   </div>
@@ -67,30 +57,24 @@ export default {
       const option = {
       const option = {
         tooltip: {
         tooltip: {
           trigger: 'item',
           trigger: 'item',
-          backgroundColor: 'rgba(13,34,71,0.9)',
-          borderColor: '#1a3a6a',
-          textStyle: { color: '#fff', fontSize: 12 }
+          backgroundColor: 'rgba(6,22,56,0.9)',
+          borderColor: 'rgba(72,180,255,0.3)',
+          textStyle: { color: '#d8f4ff' }
         },
         },
         legend: {
         legend: {
           orient: 'vertical',
           orient: 'vertical',
-          right: 10,
+          right: 4,
           top: 'center',
           top: 'center',
-          textStyle: { color: '#8cb8e6', fontSize: 11 },
-          itemWidth: 10,
-          itemHeight: 10,
-          itemGap: 10,
-          icon: 'circle'
+          textStyle: { color: '#7eacc8', fontSize: 10 },
+          itemWidth: 8,
+          itemHeight: 8
         },
         },
         series: [{
         series: [{
           type: 'pie',
           type: 'pie',
-          radius: ['35%', '60%'],
+          radius: ['30%', '60%'],
           center: ['35%', '50%'],
           center: ['35%', '50%'],
-          avoidLabelOverlap: false,
           label: { show: false },
           label: { show: false },
-          emphasis: {
-            label: { show: false },
-            scaleSize: 5
-          },
+          emphasis: { scaleSize: 4 },
           data: categories.map(c => ({
           data: categories.map(c => ({
             name: c.name,
             name: c.name,
             value: c.value,
             value: c.value,
@@ -106,75 +90,89 @@ export default {
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
 .equipment-stats {
 .equipment-stats {
-  height: 100%;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
-
-  .panel-content {
-    flex: 1;
-    display: flex;
-    flex-direction: column;
-    overflow: hidden;
-  }
 }
 }
 
 
-.chart-area {
+.equip-chart-wrap {
   flex: 1;
   flex: 1;
   min-height: 0;
   min-height: 0;
   width: 100%;
   width: 100%;
 }
 }
 
 
-.stats-row {
+.equip-mid {
   display: flex;
   display: flex;
-  gap: 8px;
+  gap: 10px;
+  justify-content: center;
+  margin: 10px 0;
   flex-shrink: 0;
   flex-shrink: 0;
 }
 }
 
 
-.top-stats {
-  margin-bottom: 8px;
+.equip-mid-item {
+  text-align: center;
+  flex: 1;
+  padding: 8px 4px;
+  border-radius: 4px;
+  background: rgba(72,215,255,0.04);
+  border: 1px solid rgba(72,215,255,0.08);
+  transition: all 0.3s ease;
 
 
-  .stat-item {
-    flex: 1;
-    text-align: center;
-    padding: 8px 4px;
-    background: rgba(255, 255, 255, 0.03);
-    border: 1px solid rgba(255, 255, 255, 0.05);
-    border-radius: 4px;
+  &:hover {
+    border-color: rgba(72,215,255,0.3);
+    box-shadow: 0 0 12px rgba(72,215,255,0.1);
+    transform: translateY(-2px);
   }
   }
-}
 
 
-.bottom-stats {
-  .stat-item {
-    flex: 1;
-    text-align: center;
-    padding: 8px 4px;
-    background: rgba(255, 255, 255, 0.03);
-    border: 1px solid rgba(255, 255, 255, 0.05);
-    border-radius: 4px;
+  .em-value {
+    font-size: 24px;
+    font-weight: 700;
+    color: $accent;
+    font-family: 'Courier New', monospace;
   }
   }
-}
 
 
-.stat-value {
-  font-size: 22px;
-  font-weight: bold;
-  font-family: 'Courier New', monospace;
-  line-height: 1.2;
+  .em-unit {
+    font-size: 11px;
+    color: $text-secondary;
+  }
 
 
-  &.c-main { color: #47D7FF; }
-  &.c-idle { color: #33C391; }
-  &.c-normal { color: #EDA525; }
-  &.c-repair { color: #E9494E; }
+  .em-label {
+    font-size: 12px;
+    color: $text-secondary;
+    margin-top: 2px;
+  }
 }
 }
 
 
-.stat-unit {
-  font-size: 12px;
-  font-weight: normal;
-  opacity: 0.7;
+.equip-bottom {
+  display: flex;
+  gap: 10px;
+  flex-shrink: 0;
 }
 }
 
 
-.stat-label {
-  font-size: 11px;
-  color: $text-muted;
-  margin-top: 3px;
+.equip-status-card {
+  flex: 1;
+  text-align: center;
+  padding: 12px 6px;
+  border-radius: 4px;
+  background: rgba(72,215,255,0.04);
+  border: 1px solid rgba(72,215,255,0.08);
+  transition: all 0.3s ease;
+
+  &:hover {
+    border-color: rgba(72,215,255,0.3);
+    box-shadow: 0 0 12px rgba(72,215,255,0.1);
+    transform: translateY(-2px);
+  }
+
+  .es-value {
+    font-size: 22px;
+    font-weight: 700;
+    font-family: 'Courier New', monospace;
+  }
+
+  .es-label {
+    font-size: 12px;
+    color: $text-secondary;
+    margin-top: 2px;
+  }
 }
 }
 </style>
 </style>

+ 64 - 75
src/components/LabStats/PersonStats.vue

@@ -1,22 +1,20 @@
 <template>
 <template>
   <div class="panel-box person-stats">
   <div class="panel-box person-stats">
+    <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
+    <div class="corner-deco bl"></div><div class="corner-deco br"></div>
     <div class="panel-title">实验室进入人数统计及走势</div>
     <div class="panel-title">实验室进入人数统计及走势</div>
     <div class="panel-content">
     <div class="panel-content">
-      <div class="counter-row">
-        <div class="counter-group">
-          <span class="counter-label">今日总进入人数</span>
-          <div class="digit-boxes">
-            <div class="digit-box" v-for="(d, i) in todayTotal" :key="'t'+i">
-              <span class="digit">{{ d }}</span>
-            </div>
+      <div class="flip-counter-row">
+        <div class="flip-group">
+          <div class="flip-label">今日总进入人数</div>
+          <div class="flip-digits">
+            <div class="flip-digit" v-for="(d, i) in todayTotal" :key="'t'+i">{{ d }}</div>
           </div>
           </div>
         </div>
         </div>
-        <div class="counter-group">
-          <span class="counter-label">当前正在实验人数</span>
-          <div class="digit-boxes">
-            <div class="digit-box" v-for="(d, i) in currentLab" :key="'c'+i">
-              <span class="digit">{{ d }}</span>
-            </div>
+        <div class="flip-group">
+          <div class="flip-label">当前正在实验人数</div>
+          <div class="flip-digits">
+            <div class="flip-digit" v-for="(d, i) in currentLab" :key="'c'+i">{{ d }}</div>
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
@@ -53,64 +51,61 @@ export default {
       const option = {
       const option = {
         tooltip: {
         tooltip: {
           trigger: 'axis',
           trigger: 'axis',
-          backgroundColor: 'rgba(13,34,71,0.9)',
-          borderColor: '#1a3a6a',
-          textStyle: { color: '#fff', fontSize: 12 }
+          backgroundColor: 'rgba(6,22,56,0.9)',
+          borderColor: 'rgba(72,180,255,0.3)',
+          textStyle: { color: '#d8f4ff' }
         },
         },
         legend: {
         legend: {
+          data: ['进入人数', '实验人数'],
           top: 0,
           top: 0,
           right: 0,
           right: 0,
-          textStyle: { color: '#8cb8e6', fontSize: 11 },
-          itemWidth: 16,
-          itemHeight: 2,
-          itemGap: 16
+          textStyle: { color: '#7eacc8', fontSize: 10 },
+          itemWidth: 14,
+          itemHeight: 2
         },
         },
-        grid: { left: 36, right: 12, top: 28, bottom: 22 },
+        grid: { left: 35, right: 10, top: 24, bottom: 20 },
         xAxis: {
         xAxis: {
           type: 'category',
           type: 'category',
           data: data.map(d => d.hour),
           data: data.map(d => d.hour),
           boundaryGap: false,
           boundaryGap: false,
-          axisLine: { lineStyle: { color: '#1a3a6a' } },
-          axisLabel: { color: '#5a7da8', fontSize: 10 },
-          axisTick: { show: false }
+          axisLabel: { color: '#7eacc8', fontSize: 10 },
+          axisLine: { lineStyle: { color: 'rgba(72,180,255,0.2)' } }
         },
         },
         yAxis: {
         yAxis: {
           type: 'value',
           type: 'value',
-          max: 150,
-          interval: 30,
-          splitLine: { lineStyle: { color: 'rgba(26,58,106,0.3)', type: 'dashed' } },
-          axisLine: { show: false },
-          axisLabel: { color: '#5a7da8', fontSize: 11 },
-          axisTick: { show: false }
+          axisLabel: { color: '#7eacc8', fontSize: 10 },
+          splitLine: { lineStyle: { color: 'rgba(72,180,255,0.08)' } }
         },
         },
         series: [
         series: [
           {
           {
             name: '进入人数',
             name: '进入人数',
             type: 'line',
             type: 'line',
             smooth: true,
             smooth: true,
-            symbol: 'none',
-            lineStyle: { color: '#2196f3', width: 2 },
+            data: data.map(d => d.enter),
+            lineStyle: { color: '#48d7ff', width: 2 },
             areaStyle: {
             areaStyle: {
-              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-                { offset: 0, color: 'rgba(33,150,243,0.25)' },
-                { offset: 1, color: 'rgba(33,150,243,0.02)' }
+              color: new echarts.graphic.LinearGradient(0,0,0,1,[
+                { offset: 0, color: 'rgba(72,215,255,0.25)' },
+                { offset: 1, color: 'rgba(72,215,255,0)' }
               ])
               ])
             },
             },
-            data: data.map(d => d.enter)
+            itemStyle: { color: '#48d7ff' },
+            symbolSize: 4
           },
           },
           {
           {
             name: '实验人数',
             name: '实验人数',
             type: 'line',
             type: 'line',
             smooth: true,
             smooth: true,
-            symbol: 'none',
-            lineStyle: { color: '#00e5ff', width: 2 },
+            data: data.map(d => d.lab),
+            lineStyle: { color: '#3a7bff', width: 2 },
             areaStyle: {
             areaStyle: {
-              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-                { offset: 0, color: 'rgba(0,229,255,0.15)' },
-                { offset: 1, color: 'rgba(0,229,255,0.02)' }
+              color: new echarts.graphic.LinearGradient(0,0,0,1,[
+                { offset: 0, color: 'rgba(58,123,255,0.2)' },
+                { offset: 1, color: 'rgba(58,123,255,0)' }
               ])
               ])
             },
             },
-            data: data.map(d => d.lab)
+            itemStyle: { color: '#3a7bff' },
+            symbolSize: 4
           }
           }
         ]
         ]
       }
       }
@@ -122,59 +117,53 @@ export default {
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
 .person-stats {
 .person-stats {
-  height: 100%;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
-
-  .panel-content {
-    flex: 1;
-    display: flex;
-    flex-direction: column;
-    overflow: hidden;
-  }
 }
 }
 
 
-.counter-row {
+.flip-counter-row {
   display: flex;
   display: flex;
-  justify-content: space-between;
-  gap: 16px;
+  gap: 24px;
+  justify-content: center;
+  margin-bottom: 10px;
   flex-shrink: 0;
   flex-shrink: 0;
-  margin-bottom: 8px;
 }
 }
 
 
-.counter-group {
-  display: flex;
-  flex-direction: column;
-  gap: 6px;
+.flip-group {
+  text-align: center;
 }
 }
 
 
-.counter-label {
+.flip-label {
   font-size: 12px;
   font-size: 12px;
   color: $text-secondary;
   color: $text-secondary;
+  margin-bottom: 6px;
 }
 }
 
 
-.digit-boxes {
+.flip-digits {
   display: flex;
   display: flex;
-  gap: 6px;
-}
-
-.digit-box {
-  width: 36px;
-  height: 44px;
-  background: linear-gradient(180deg, rgba(13,34,71,0.9), rgba(10,26,58,0.9));
-  border: 1px solid rgba(30, 144, 255, 0.25);
-  border-radius: 3px;
-  display: flex;
-  align-items: center;
+  gap: 4px;
   justify-content: center;
   justify-content: center;
 }
 }
 
 
-.digit {
+.flip-digit {
+  width: 32px;
+  height: 42px;
+  line-height: 42px;
+  text-align: center;
   font-size: 26px;
   font-size: 26px;
-  font-weight: bold;
-  color: $accent-cyan;
+  font-weight: 700;
+  color: $accent;
+  background: linear-gradient(180deg, rgba(72,215,255,0.12) 0%, rgba(6,22,56,0.9) 100%);
+  border: 1px solid rgba(72,215,255,0.25);
+  border-radius: 4px;
   font-family: 'Courier New', monospace;
   font-family: 'Courier New', monospace;
-  text-shadow: 0 0 10px rgba(0, 229, 255, 0.4);
+  animation: digitGlow 2s ease-in-out infinite alternate;
+  transition: all 0.3s ease;
+}
+
+@keyframes digitGlow {
+  0% { box-shadow: 0 0 4px rgba(72,215,255,0.1); text-shadow: 0 0 4px rgba(72,215,255,0.3); }
+  100% { box-shadow: 0 0 10px rgba(72,215,255,0.25); text-shadow: 0 0 12px rgba(72,215,255,0.5); }
 }
 }
 
 
 .chart-area {
 .chart-area {

+ 69 - 83
src/components/LabStats/RiskWarning.vue

@@ -1,31 +1,19 @@
 <template>
 <template>
   <div class="panel-box risk-warning">
   <div class="panel-box risk-warning">
+    <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
+    <div class="corner-deco bl"></div><div class="corner-deco br"></div>
     <div class="panel-title">实验室实时风险预警</div>
     <div class="panel-title">实验室实时风险预警</div>
     <div class="panel-content">
     <div class="panel-content">
-      <div class="warning-summary">
-        <span class="summary-num">{{ totalMonth }}</span>
-        <span class="summary-text">本月预警响应总数</span>
+      <div class="warning-header">
+        <div class="warning-count">{{ totalMonth }}</div>
+        <div class="warning-count-label">本月预警响应总数</div>
       </div>
       </div>
-      <div class="list-wrapper" ref="listWrapper" @mouseenter="pauseScroll" @mouseleave="resumeScroll">
-        <div ref="scrollContent" class="scroll-content">
-          <div ref="listInner" class="list-inner">
-            <div class="warning-item" v-for="(item, idx) in list" :key="'a'+idx">
-              <div class="warning-lab">{{ item.lab }} - {{ item.dept }}</div>
-              <div class="warning-detail">
-                异常: <span class="warn-value">{{ item.type }} {{ item.value }}</span>
-              </div>
-              <div class="warning-time">{{ item.time }}</div>
-            </div>
-          </div>
-          <!-- 复制一份用于无缝滚动 -->
-          <div class="list-inner" v-if="needScroll">
-            <div class="warning-item" v-for="(item, idx) in list" :key="'b'+idx">
-              <div class="warning-lab">{{ item.lab }} - {{ item.dept }}</div>
-              <div class="warning-detail">
-                异常: <span class="warn-value">{{ item.type }} {{ item.value }}</span>
-              </div>
-              <div class="warning-time">{{ item.time }}</div>
-            </div>
+      <div class="warning-scroll-wrap" ref="scrollWrap" @mouseenter="paused=true" @mouseleave="paused=false">
+        <div ref="scrollInner" class="warning-scroll-inner">
+          <div class="warning-item" v-for="(item, idx) in displayList" :key="idx">
+            <div class="w-lab">{{ item.lab }} - {{ item.dept }}</div>
+            <div class="w-sensor">异常: {{ item.type }} {{ item.value }}</div>
+            <div class="w-time">{{ item.time }}</div>
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
@@ -42,128 +30,126 @@ export default {
     return {
     return {
       totalMonth: 0,
       totalMonth: 0,
       list: [],
       list: [],
-      needScroll: false,
-      scrollTimer: null,
       paused: false,
       paused: false,
+      scrollTimer: null,
       scrollY: 0
       scrollY: 0
     }
     }
   },
   },
+  computed: {
+    displayList() {
+      return [...this.list, ...this.list]
+    }
+  },
   async mounted() {
   async mounted() {
     const res = await getRiskWarningList()
     const res = await getRiskWarningList()
     this.totalMonth = res.data.totalMonth
     this.totalMonth = res.data.totalMonth
     this.list = res.data.list
     this.list = res.data.list
     this.$nextTick(() => {
     this.$nextTick(() => {
-      setTimeout(() => this.checkScroll(), 300)
+      setTimeout(() => this.startScroll(), 500)
     })
     })
   },
   },
   beforeDestroy() {
   beforeDestroy() {
     if (this.scrollTimer) cancelAnimationFrame(this.scrollTimer)
     if (this.scrollTimer) cancelAnimationFrame(this.scrollTimer)
   },
   },
   methods: {
   methods: {
-    checkScroll() {
-      const wrapper = this.$refs.listWrapper
-      const inner = this.$refs.listInner
-      if (!wrapper || !inner) return
-      this.needScroll = inner.offsetHeight > wrapper.offsetHeight
-      if (this.needScroll) {
-        this.$nextTick(() => {
-          setTimeout(() => this.startScroll(), 100)
-        })
-      }
-    },
     startScroll() {
     startScroll() {
-      const inner = this.$refs.listInner
-      const scrollContent = this.$refs.scrollContent
-      if (!inner || !scrollContent) return
-      const firstHeight = inner.offsetHeight
+      const wrap = this.$refs.scrollWrap
+      const inner = this.$refs.scrollInner
+      if (!wrap || !inner) return
+      const halfHeight = inner.scrollHeight / 2
+      if (halfHeight <= wrap.offsetHeight) return
+
       const speed = 0.4
       const speed = 0.4
       this.scrollY = 0
       this.scrollY = 0
 
 
       const step = () => {
       const step = () => {
         if (!this.paused) {
         if (!this.paused) {
           this.scrollY += speed
           this.scrollY += speed
-          if (this.scrollY >= firstHeight) {
-            this.scrollY -= firstHeight
-          }
-          scrollContent.style.transform = `translateY(${-this.scrollY}px)`
+          if (this.scrollY >= halfHeight) this.scrollY -= halfHeight
+          inner.style.transform = `translateY(${-this.scrollY}px)`
         }
         }
         this.scrollTimer = requestAnimationFrame(step)
         this.scrollTimer = requestAnimationFrame(step)
       }
       }
       this.scrollTimer = requestAnimationFrame(step)
       this.scrollTimer = requestAnimationFrame(step)
-    },
-    pauseScroll() { this.paused = true },
-    resumeScroll() { this.paused = false }
+    }
   }
   }
 }
 }
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
 .risk-warning {
 .risk-warning {
-  height: 100%;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
-
-  .panel-content {
-    flex: 1;
-    display: flex;
-    flex-direction: column;
-    overflow: hidden;
-  }
 }
 }
 
 
-.warning-summary {
+.warning-header {
   display: flex;
   display: flex;
-  align-items: baseline;
-  gap: 10px;
+  align-items: center;
+  gap: 12px;
   margin-bottom: 10px;
   margin-bottom: 10px;
   flex-shrink: 0;
   flex-shrink: 0;
 }
 }
 
 
-.summary-num {
+.warning-count {
   font-size: 28px;
   font-size: 28px;
-  font-weight: bold;
-  color: #FFB020;
+  font-weight: 700;
+  color: $accent-orange;
   font-family: 'Courier New', monospace;
   font-family: 'Courier New', monospace;
+  text-shadow: 0 0 10px rgba(255,176,32,0.4);
+  animation: warnCountGlow 2s ease-in-out infinite;
 }
 }
 
 
-.summary-text {
-  font-size: 13px;
-  color: $text-muted;
+@keyframes warnCountGlow {
+  0%, 100% { text-shadow: 0 0 8px rgba(255,176,32,0.3); }
+  50% { text-shadow: 0 0 20px rgba(255,176,32,0.6), 0 0 40px rgba(255,176,32,0.2); }
 }
 }
 
 
-.list-wrapper {
+.warning-count-label {
+  font-size: 12px;
+  color: $text-secondary;
+}
+
+.warning-scroll-wrap {
   flex: 1;
   flex: 1;
   overflow: hidden;
   overflow: hidden;
+  min-height: 0;
 }
 }
 
 
 .warning-item {
 .warning-item {
-  padding: 10px 12px;
-  margin-bottom: 8px;
+  padding: 8px 12px;
+  margin-bottom: 6px;
+  border-radius: 4px;
+  background: rgba(255,176,32,0.04);
   border-left: 3px solid $accent-orange;
   border-left: 3px solid $accent-orange;
-  background: rgba(255, 152, 0, 0.04);
-  border-radius: 0 4px 4px 0;
+  transition: all 0.3s ease;
+  animation: warnBorderPulse 3s ease-in-out infinite;
+
+  &:hover {
+    background: rgba(255,176,32,0.08);
+    transform: translateX(3px);
+  }
+}
+
+@keyframes warnBorderPulse {
+  0%, 100% { border-left-color: rgba(255,176,32,0.6); }
+  50% { border-left-color: $accent-orange; box-shadow: -2px 0 8px rgba(255,176,32,0.15); }
 }
 }
 
 
-.warning-lab {
+.w-lab {
   font-size: 13px;
   font-size: 13px;
-  font-weight: bold;
+  font-weight: 600;
   color: #fff;
   color: #fff;
-  margin-bottom: 4px;
 }
 }
 
 
-.warning-detail {
+.w-sensor {
   font-size: 12px;
   font-size: 12px;
-  color: #FFB020;
-  margin-bottom: 3px;
-
-  .warn-value {
-    color: #FFB020;
-    font-weight: bold;
-  }
+  color: $accent-orange;
+  margin: 2px 0;
 }
 }
 
 
-.warning-time {
+.w-time {
   font-size: 11px;
   font-size: 11px;
-  color: $text-muted;
+  color: $text-secondary;
+  margin-top: 4px;
 }
 }
 </style>
 </style>

+ 45 - 91
src/components/LabStats/SafetyLevel.vue

@@ -1,13 +1,9 @@
 <template>
 <template>
   <div class="panel-box safety-level">
   <div class="panel-box safety-level">
+    <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
+    <div class="corner-deco bl"></div><div class="corner-deco br"></div>
     <div class="panel-title">实验室安全分级统计</div>
     <div class="panel-title">实验室安全分级统计</div>
     <div class="panel-content">
     <div class="panel-content">
-      <div class="level-legend">
-        <div class="legend-item" v-for="item in legendItems" :key="item.name">
-          <span class="legend-dot" :style="{ background: item.color }"></span>
-          <span class="legend-text">{{ item.name }}</span>
-        </div>
-      </div>
       <div ref="chart" class="chart-area"></div>
       <div ref="chart" class="chart-area"></div>
     </div>
     </div>
   </div>
   </div>
@@ -20,15 +16,7 @@ import { getSafetyLevelStats } from '@/api'
 export default {
 export default {
   name: 'SafetyLevel',
   name: 'SafetyLevel',
   data() {
   data() {
-    return {
-      chart: null,
-      legendItems: [
-        { name: 'I级', color: '#f44336' },
-        { name: 'II级', color: '#ff9800' },
-        { name: 'III级', color: '#ffc107' },
-        { name: 'IV级', color: '#2196f3' }
-      ]
-    }
+    return { chart: null, scrollTimer: null }
   },
   },
   async mounted() {
   async mounted() {
     const res = await getSafetyLevelStats()
     const res = await getSafetyLevelStats()
@@ -36,69 +24,68 @@ export default {
   },
   },
   beforeDestroy() {
   beforeDestroy() {
     if (this.chart) this.chart.dispose()
     if (this.chart) this.chart.dispose()
+    if (this.scrollTimer) clearInterval(this.scrollTimer)
   },
   },
   methods: {
   methods: {
     initChart(data) {
     initChart(data) {
       this.chart = echarts.init(this.$refs.chart)
       this.chart = echarts.init(this.$refs.chart)
       const depts = data.departments
       const depts = data.departments
+      const names = depts.map(d => d.name)
+      const totals = depts.map(d => d.total)
+
       const option = {
       const option = {
         tooltip: {
         tooltip: {
           trigger: 'axis',
           trigger: 'axis',
-          backgroundColor: 'rgba(13,34,71,0.9)',
-          borderColor: '#1a3a6a',
-          textStyle: { color: '#fff', fontSize: 12 }
+          axisPointer: { type: 'shadow' },
+          backgroundColor: 'rgba(6,22,56,0.9)',
+          borderColor: 'rgba(72,180,255,0.3)',
+          textStyle: { color: '#d8f4ff' }
+        },
+        legend: {
+          data: ['I级', 'II级', 'III级', 'IV级'],
+          top: 0,
+          textStyle: { color: '#7eacc8', fontSize: 11 },
+          itemWidth: 10,
+          itemHeight: 10
         },
         },
-        legend: { show: false },
-        grid: { left: 12, right: 12, top: 16, bottom: 50 },
+        grid: { left: 40, right: 10, top: 35, bottom: 50 },
         xAxis: {
         xAxis: {
           type: 'category',
           type: 'category',
-          data: depts.map(d => d.name + '\n(' + d.total + ')'),
-          axisLine: { lineStyle: { color: '#1a3a6a' } },
-          axisLabel: { color: '#5a7da8', fontSize: 10, interval: 0, lineHeight: 14 },
+          data: names,
+          axisLabel: {
+            color: '#7eacc8',
+            fontSize: 10,
+            interval: 0,
+            formatter: (v, idx) => v + '\n(' + totals[idx] + ')'
+          },
+          axisLine: { lineStyle: { color: 'rgba(72,180,255,0.2)' } },
           axisTick: { show: false }
           axisTick: { show: false }
         },
         },
         yAxis: {
         yAxis: {
           type: 'value',
           type: 'value',
-          max: 15,
-          interval: 3,
-          splitLine: { lineStyle: { color: 'rgba(26,58,106,0.3)', type: 'dashed' } },
-          axisLine: { show: false },
-          axisLabel: { color: '#5a7da8', fontSize: 11 },
-          axisTick: { show: false }
+          axisLabel: { color: '#7eacc8', fontSize: 10 },
+          axisLine: { lineStyle: { color: 'rgba(72,180,255,0.2)' } },
+          splitLine: { lineStyle: { color: 'rgba(72,180,255,0.08)' } }
         },
         },
+        dataZoom: [{ type: 'slider', show: false, xAxisIndex: 0, startValue: 0, endValue: 5 }],
         series: [
         series: [
-          {
-            name: 'I级',
-            type: 'bar',
-            stack: 'total',
-            barWidth: 24,
-            data: depts.map(d => d.level1),
-            itemStyle: { color: '#f44336' }
-          },
-          {
-            name: 'II级',
-            type: 'bar',
-            stack: 'total',
-            data: depts.map(d => d.level2),
-            itemStyle: { color: '#ff9800' }
-          },
-          {
-            name: 'III级',
-            type: 'bar',
-            stack: 'total',
-            data: depts.map(d => d.level3),
-            itemStyle: { color: '#ffc107' }
-          },
-          {
-            name: 'IV级',
-            type: 'bar',
-            stack: 'total',
-            data: depts.map(d => d.level4),
-            itemStyle: { color: '#2196f3', borderRadius: [3, 3, 0, 0] }
-          }
+          { name: 'I级', type: 'bar', stack: 'total', barWidth: 20, data: depts.map(d => d.level1), itemStyle: { color: '#ff4d4f' } },
+          { name: 'II级', type: 'bar', stack: 'total', data: depts.map(d => d.level2), itemStyle: { color: '#ff8c00' } },
+          { name: 'III级', type: 'bar', stack: 'total', data: depts.map(d => d.level3), itemStyle: { color: '#ffcc00' } },
+          { name: 'IV级', type: 'bar', stack: 'total', data: depts.map(d => d.level4), itemStyle: { color: '#3a7bff', borderRadius: [3,3,0,0] } }
         ]
         ]
       }
       }
       this.chart.setOption(option)
       this.chart.setOption(option)
+
+      // 自动滚动X轴
+      if (depts.length > 6) {
+        let idx = 0
+        this.scrollTimer = setInterval(() => {
+          idx++
+          if (idx > depts.length - 5) idx = 0
+          this.chart.dispatchAction({ type: 'dataZoom', startValue: idx, endValue: idx + 5 })
+        }, 5000)
+      }
     }
     }
   }
   }
 }
 }
@@ -106,41 +93,8 @@ export default {
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
 .safety-level {
 .safety-level {
-  height: 100%;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
-
-  .panel-content {
-    flex: 1;
-    display: flex;
-    flex-direction: column;
-    overflow: hidden;
-  }
-}
-
-.level-legend {
-  display: flex;
-  justify-content: center;
-  gap: 16px;
-  flex-shrink: 0;
-  margin-bottom: 4px;
-}
-
-.legend-item {
-  display: flex;
-  align-items: center;
-  gap: 5px;
-}
-
-.legend-dot {
-  width: 8px;
-  height: 8px;
-  border-radius: 50%;
-}
-
-.legend-text {
-  font-size: 11px;
-  color: $text-secondary;
 }
 }
 
 
 .chart-area {
 .chart-area {

+ 74 - 31
src/components/VideoMonitor/BuildingNav.vue

@@ -1,23 +1,28 @@
 <template>
 <template>
   <div class="panel-box building-nav">
   <div class="panel-box building-nav">
+    <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
+    <div class="corner-deco bl"></div><div class="corner-deco br"></div>
     <div class="panel-title">建筑结构导航</div>
     <div class="panel-title">建筑结构导航</div>
     <div class="panel-content">
     <div class="panel-content">
-      <el-tree
-        :data="treeData"
-        :props="{ label: 'label', children: 'children' }"
-        node-key="id"
-        default-expand-all
-        highlight-current
-        @node-click="handleNodeClick"
-        class="dark-tree"
-      >
-        <template #default="{ node }">
-          <span class="tree-node">
-            <i :class="node.isLeaf ? 'el-icon-video-camera' : 'el-icon-folder-opened'"></i>
-            <span>{{ node.label }}</span>
-          </span>
-        </template>
-      </el-tree>
+      <input class="video-search" type="text" placeholder="搜索楼栋 / 楼层..." />
+      <div class="tree-container">
+        <el-tree
+          :data="treeData"
+          :props="{ label: 'label', children: 'children' }"
+          node-key="id"
+          default-expand-all
+          highlight-current
+          @node-click="handleNodeClick"
+          class="dark-tree"
+        >
+          <template #default="{ node }">
+            <span class="tree-node-label">
+              <i :class="node.isLeaf ? 'el-icon-video-camera' : 'el-icon-folder-opened'"></i>
+              <span>{{ node.label }}</span>
+            </span>
+          </template>
+        </el-tree>
+      </div>
     </div>
     </div>
   </div>
   </div>
 </template>
 </template>
@@ -28,9 +33,7 @@ import { getBuildingTree } from '@/api'
 export default {
 export default {
   name: 'BuildingNav',
   name: 'BuildingNav',
   data() {
   data() {
-    return {
-      treeData: []
-    }
+    return { treeData: [] }
   },
   },
   async mounted() {
   async mounted() {
     const res = await getBuildingTree()
     const res = await getBuildingTree()
@@ -52,10 +55,39 @@ export default {
 
 
   .panel-content {
   .panel-content {
     flex: 1;
     flex: 1;
-    overflow-y: auto;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+  }
+}
+
+.video-search {
+  width: 100%;
+  padding: 8px 12px;
+  border-radius: 4px;
+  background: rgba(72,215,255,0.06);
+  border: 1px solid $border-color;
+  color: $text-primary;
+  font-size: 13px;
+  outline: none;
+  margin-bottom: 10px;
+  flex-shrink: 0;
+  transition: all 0.3s ease;
+
+  &::placeholder { color: $text-secondary; }
+
+  &:focus {
+    border-color: $accent;
+    box-shadow: 0 0 12px rgba(72,215,255,0.2), inset 0 0 8px rgba(72,215,255,0.06);
+    background: rgba(72,215,255,0.1);
   }
   }
 }
 }
 
 
+.tree-container {
+  flex: 1;
+  overflow-y: auto;
+}
+
 .dark-tree {
 .dark-tree {
   background: transparent;
   background: transparent;
   color: $text-secondary;
   color: $text-secondary;
@@ -63,33 +95,44 @@ export default {
   ::v-deep {
   ::v-deep {
     .el-tree-node__content {
     .el-tree-node__content {
       background: transparent;
       background: transparent;
-      height: 36px;
+      height: 34px;
+      transition: all 0.3s ease;
+      position: relative;
+      overflow: hidden;
 
 
       &:hover {
       &:hover {
-        background: rgba(30, 144, 255, 0.1);
+        background: rgba(72,215,255,0.08);
+        box-shadow: 0 0 8px rgba(72,215,255,0.08);
       }
       }
     }
     }
 
 
     .is-current > .el-tree-node__content {
     .is-current > .el-tree-node__content {
-      background: rgba(30, 144, 255, 0.2);
-      color: $accent-cyan;
+      background: rgba(72,215,255,0.15);
+      color: $accent;
+      border-left: 2px solid $accent;
+      animation: treeSelectedGlow 2.5s ease-in-out infinite;
     }
     }
 
 
     .el-tree-node__expand-icon {
     .el-tree-node__expand-icon {
       color: $text-muted;
       color: $text-muted;
+      transition: all 0.3s ease;
 
 
-      &.is-leaf {
-        color: transparent;
+      &.is-leaf { color: transparent; }
+      &:not(.is-leaf) { color: $accent-blue; }
+      &.expanded {
+        color: $accent;
+        text-shadow: 0 0 6px rgba(72,215,255,0.4);
       }
       }
     }
     }
-
-    .el-tree-node__expand-icon:not(.is-leaf) {
-      color: $accent-blue;
-    }
   }
   }
 }
 }
 
 
-.tree-node {
+@keyframes treeSelectedGlow {
+  0%, 100% { box-shadow: 0 0 6px rgba(72,215,255,0.1); background: rgba(72,215,255,0.12); }
+  50% { box-shadow: 0 0 14px rgba(72,215,255,0.2); background: rgba(72,215,255,0.2); }
+}
+
+.tree-node-label {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   gap: 6px;
   gap: 6px;

+ 243 - 59
src/components/VideoMonitor/VideoGrid.vue

@@ -1,5 +1,7 @@
 <template>
 <template>
   <div class="panel-box video-grid">
   <div class="panel-box video-grid">
+    <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
+    <div class="corner-deco bl"></div><div class="corner-deco br"></div>
     <div class="panel-title">
     <div class="panel-title">
       实时视频监控
       实时视频监控
       <span class="video-count">共 {{ total }} 路</span>
       <span class="video-count">共 {{ total }} 路</span>
@@ -7,27 +9,37 @@
     <div class="panel-content">
     <div class="panel-content">
       <div class="grid-container">
       <div class="grid-container">
         <div
         <div
-          v-for="video in videoList"
+          v-for="(video, idx) in videoList"
           :key="video.id"
           :key="video.id"
-          class="video-cell"
+          class="cam-frame"
+          :class="{ 'ai-cam': video.ai }"
+          :style="{ animationDelay: (idx * 0.08) + 's' }"
         >
         >
-          <div class="video-placeholder">
-            <div class="video-glow"></div>
-            <i class="el-icon-video-camera"></i>
+          <div class="cam-inner">
+            <div class="cam-video-area">
+              <i class="el-icon-video-camera cam-icon"></i>
+              <div class="cam-scan-line"></div>
+            </div>
+            <div class="cam-corner tl"></div>
+            <div class="cam-corner tr"></div>
+            <div class="cam-corner bl"></div>
+            <div class="cam-corner br"></div>
+          </div>
+          <div class="cam-label">
+            <span class="cam-name">{{ video.name }}</span>
+            <span v-if="video.ai" class="cam-ai-badge">AI</span>
+            <span class="cam-status" :class="video.status">{{ video.status === 'online' ? 'REC' : 'OFF' }}</span>
           </div>
           </div>
-          <div class="video-label">{{ video.name }}</div>
         </div>
         </div>
       </div>
       </div>
       <div class="grid-pagination">
       <div class="grid-pagination">
-        <el-pagination
-          small
-          layout="prev, pager, next"
-          :total="total"
-          :page-size="9"
-          :current-page.sync="currentPage"
-          @current-change="loadVideos"
-          class="dark-pagination"
-        />
+        <button class="page-btn" :disabled="currentPage <= 1" @click="changePage(-1)">
+          <i class="el-icon-arrow-left"></i>
+        </button>
+        <span class="page-info">{{ currentPage }} / {{ totalPages }}</span>
+        <button class="page-btn" :disabled="currentPage >= totalPages" @click="changePage(1)">
+          <i class="el-icon-arrow-right"></i>
+        </button>
       </div>
       </div>
     </div>
     </div>
   </div>
   </div>
@@ -45,6 +57,11 @@ export default {
       currentPage: 1
       currentPage: 1
     }
     }
   },
   },
+  computed: {
+    totalPages() {
+      return Math.ceil(this.total / 9) || 1
+    }
+  },
   mounted() {
   mounted() {
     this.loadVideos()
     this.loadVideos()
   },
   },
@@ -53,6 +70,10 @@ export default {
       const res = await getVideoList({ page: this.currentPage })
       const res = await getVideoList({ page: this.currentPage })
       this.videoList = res.data.list
       this.videoList = res.data.list
       this.total = res.data.total
       this.total = res.data.total
+    },
+    changePage(delta) {
+      this.currentPage += delta
+      this.loadVideos()
     }
     }
   }
   }
 }
 }
@@ -83,78 +104,241 @@ export default {
   display: grid;
   display: grid;
   grid-template-columns: repeat(3, 1fr);
   grid-template-columns: repeat(3, 1fr);
   grid-template-rows: repeat(3, 1fr);
   grid-template-rows: repeat(3, 1fr);
-  gap: 12px;
+  gap: 10px;
 }
 }
 
 
-.video-cell {
+/* ---- Camera Frame ---- */
+.cam-frame {
   position: relative;
   position: relative;
-  border: 1px solid $border-color;
-  border-radius: 4px;
-  overflow: hidden;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
+  animation: camEntrance 0.5s ease-out both;
 }
 }
 
 
-.video-placeholder {
+@keyframes camEntrance {
+  from { opacity: 0; transform: scale(0.92); }
+  to { opacity: 1; transform: scale(1); }
+}
+
+.cam-inner {
   flex: 1;
   flex: 1;
-  background: rgba(10, 26, 58, 0.8);
+  position: relative;
+  background: rgba(6, 22, 56, 0.6);
+  clip-path: polygon(
+    0 8px, 8px 0, calc(100% - 8px) 0, 100% 8px,
+    100% calc(100% - 8px), calc(100% - 8px) 100%, 8px 100%, 0 calc(100% - 8px)
+  );
+  overflow: hidden;
+  border: 1px solid rgba(72, 215, 255, 0.15);
+  transition: all 0.3s ease;
+
+  &::before {
+    content: '';
+    position: absolute;
+    inset: 0;
+    border: 1px solid rgba(72, 215, 255, 0.12);
+    clip-path: inherit;
+    pointer-events: none;
+    z-index: 2;
+  }
+
+  &::after {
+    content: '';
+    position: absolute;
+    inset: -2px;
+    background: conic-gradient(from 0deg, transparent 0%, rgba(72,215,255,0.4) 10%, transparent 20%);
+    animation: camFrameBorderFlow 6s linear infinite;
+    clip-path: inherit;
+    z-index: 1;
+    opacity: 0;
+    transition: opacity 0.3s ease;
+  }
+
+  .cam-frame:hover &::after {
+    opacity: 1;
+  }
+}
+
+@keyframes camFrameBorderFlow {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+
+/* Camera corner accents */
+.cam-corner {
+  position: absolute;
+  width: 12px;
+  height: 12px;
+  z-index: 3;
+
+  &::before, &::after {
+    content: '';
+    position: absolute;
+    background: $accent;
+    box-shadow: 0 0 4px $accent;
+  }
+
+  &.tl { top: 0; left: 0;
+    &::before { top: 0; left: 0; width: 12px; height: 1px; }
+    &::after { top: 0; left: 0; width: 1px; height: 12px; }
+  }
+  &.tr { top: 0; right: 0;
+    &::before { top: 0; right: 0; width: 12px; height: 1px; }
+    &::after { top: 0; right: 0; width: 1px; height: 12px; }
+  }
+  &.bl { bottom: 0; left: 0;
+    &::before { bottom: 0; left: 0; width: 12px; height: 1px; }
+    &::after { bottom: 0; left: 0; width: 1px; height: 12px; }
+  }
+  &.br { bottom: 0; right: 0;
+    &::before { bottom: 0; right: 0; width: 12px; height: 1px; }
+    &::after { bottom: 0; right: 0; width: 1px; height: 12px; }
+  }
+}
+
+/* AI camera variant - orange accents */
+.ai-cam {
+  .cam-corner::before, .cam-corner::after {
+    background: $accent-orange;
+    box-shadow: 0 0 4px $accent-orange;
+  }
+
+  .cam-inner {
+    border-color: rgba(255, 176, 32, 0.2);
+
+    &::after {
+      background: conic-gradient(from 0deg, transparent 0%, rgba(255,176,32,0.4) 10%, transparent 20%);
+    }
+  }
+
+  .cam-scan-line {
+    background: linear-gradient(180deg, transparent, rgba(255,176,32,0.15), transparent);
+  }
+}
+
+.cam-video-area {
+  width: 100%;
+  height: 100%;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
   position: relative;
   position: relative;
-  overflow: hidden;
+}
 
 
-  i {
-    font-size: 32px;
-    color: rgba(255, 255, 255, 0.1);
-    z-index: 1;
+.cam-icon {
+  font-size: 30px;
+  color: rgba(72, 215, 255, 0.12);
+  z-index: 1;
+
+  .ai-cam & {
+    color: rgba(255, 176, 32, 0.12);
   }
   }
 }
 }
 
 
-// 蓝色光晕效果
-.video-glow {
+.cam-scan-line {
   position: absolute;
   position: absolute;
-  width: 120px;
-  height: 120px;
-  background: radial-gradient(circle, rgba(30, 144, 255, 0.15) 0%, transparent 70%);
-  border-radius: 50%;
-  animation: pulse 3s ease-in-out infinite;
+  left: 0;
+  width: 100%;
+  height: 40%;
+  background: linear-gradient(180deg, transparent, rgba(72,215,255,0.08), transparent);
+  animation: gridScan 4s ease-in-out infinite;
+  pointer-events: none;
+  z-index: 2;
 }
 }
 
 
-@keyframes pulse {
-  0%, 100% { transform: scale(0.8); opacity: 0.5; }
-  50% { transform: scale(1.2); opacity: 1; }
+@keyframes gridScan {
+  0% { top: -40%; }
+  50% { top: 100%; }
+  50.01% { top: -40%; }
+  100% { top: -40%; }
 }
 }
 
 
-.video-label {
-  padding: 6px 10px;
+.cam-label {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 5px 8px;
   font-size: 12px;
   font-size: 12px;
+  background: rgba(6, 22, 56, 0.8);
+  border-top: 1px solid rgba(72, 215, 255, 0.08);
+  flex-shrink: 0;
+}
+
+.cam-name {
   color: $text-secondary;
   color: $text-secondary;
-  background: rgba(13, 34, 71, 0.9);
-  border-top: 1px solid $border-color;
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 }
 
 
+.cam-ai-badge {
+  font-size: 10px;
+  padding: 1px 5px;
+  border-radius: 3px;
+  color: $accent-orange;
+  background: rgba(255, 176, 32, 0.15);
+  border: 1px solid rgba(255, 176, 32, 0.3);
+  font-weight: 700;
+}
+
+.cam-status {
+  font-size: 10px;
+  font-weight: 600;
+
+  &.online {
+    color: $accent-green;
+    animation: recBlink 1.5s ease-in-out infinite;
+  }
+
+  &.offline {
+    color: $text-muted;
+  }
+}
+
+@keyframes recBlink {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0.3; }
+}
+
+/* ---- Pagination ---- */
 .grid-pagination {
 .grid-pagination {
   display: flex;
   display: flex;
   justify-content: center;
   justify-content: center;
-  padding-top: 12px;
-}
-
-.dark-pagination {
-  ::v-deep {
-    .btn-prev, .btn-next, .el-pager li {
-      background: transparent;
-      color: $text-secondary;
-      border: 1px solid $border-color;
-      min-width: 28px;
-      height: 28px;
-      line-height: 28px;
-
-      &:hover, &.active {
-        color: $accent-cyan;
-        border-color: $accent-blue;
-      }
-    }
+  align-items: center;
+  gap: 14px;
+  padding-top: 10px;
+  flex-shrink: 0;
+}
+
+.page-btn {
+  width: 30px;
+  height: 30px;
+  border-radius: 4px;
+  background: rgba(72, 215, 255, 0.06);
+  border: 1px solid rgba(72, 215, 255, 0.15);
+  color: $text-secondary;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: all 0.3s ease;
+
+  &:hover:not(:disabled) {
+    border-color: $accent;
+    color: $accent;
+    box-shadow: 0 0 8px rgba(72, 215, 255, 0.2);
   }
   }
+
+  &:disabled {
+    opacity: 0.3;
+    cursor: not-allowed;
+  }
+}
+
+.page-info {
+  font-size: 13px;
+  color: $text-secondary;
+  font-family: 'Courier New', monospace;
 }
 }
 </style>
 </style>

+ 150 - 44
src/styles/global.scss

@@ -8,15 +8,17 @@ html, body {
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;
   overflow: hidden;
   overflow: hidden;
-  background: $bg-dark;
+  background: radial-gradient(ellipse 1400px 700px at 10% 0%, rgba(50,140,255,0.15), transparent 65%),
+              radial-gradient(ellipse 1000px 500px at 90% 100%, rgba(0,200,255,0.1), transparent 60%),
+              linear-gradient(150deg, $bg-0, $bg-1 45%, $bg-2);
   color: $text-primary;
   color: $text-primary;
-  font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
+  font-family: "DIN Alternate","Alibaba PuHuiTi","PingFang SC","Microsoft YaHei",sans-serif;
 }
 }
 
 
 #app {
 #app {
-  width: $screen-width;
-  height: $screen-height;
-  overflow: hidden;
+  width: 100%;
+  height: 100%;
+  position: relative;
 }
 }
 
 
 // 滚动条样式
 // 滚动条样式
@@ -25,88 +27,192 @@ html, body {
   height: 4px;
   height: 4px;
 }
 }
 ::-webkit-scrollbar-thumb {
 ::-webkit-scrollbar-thumb {
-  background: rgba(255,255,255,0.15);
+  background: $border-color;
   border-radius: 2px;
   border-radius: 2px;
+
+  &:hover {
+    background: $accent;
+    box-shadow: 0 0 8px rgba(72,215,255,0.4);
+  }
 }
 }
 ::-webkit-scrollbar-track {
 ::-webkit-scrollbar-track {
   background: transparent;
   background: transparent;
 }
 }
 
 
-// 面板通用样式
+// ======================== 视口缩放容器 ========================
+.viewport {
+  width: 100%;
+  height: 100%;
+  position: relative;
+}
+
+.screen {
+  width: 1920px;
+  height: 1080px;
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  transform-origin: center center;
+  overflow: hidden;
+}
+
+// 科技网格叠加
+.screen::before {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  pointer-events: none;
+  z-index: 0;
+  background-image:
+    linear-gradient(rgba(72,180,255,0.04) 1px, transparent 1px),
+    linear-gradient(90deg, rgba(72,180,255,0.04) 1px, transparent 1px);
+  background-size: 60px 60px;
+  mask-image: radial-gradient(ellipse 70% 60% at 50% 50%, rgba(0,0,0,0.5) 0%, transparent 100%);
+}
+
+// 环境浮动粒子
+.screen::after {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  pointer-events: none;
+  z-index: 1;
+  background:
+    radial-gradient(1.5px 1.5px at 15% 25%, rgba(72,215,255,0.5), transparent),
+    radial-gradient(1px 1px at 85% 15%, rgba(72,215,255,0.4), transparent),
+    radial-gradient(1.5px 1.5px at 45% 80%, rgba(58,123,255,0.4), transparent),
+    radial-gradient(1px 1px at 70% 55%, rgba(72,215,255,0.3), transparent),
+    radial-gradient(1.5px 1.5px at 25% 60%, rgba(100,200,255,0.35), transparent),
+    radial-gradient(1px 1px at 90% 75%, rgba(72,215,255,0.3), transparent);
+  animation: particleDrift 20s ease-in-out infinite alternate;
+}
+
+@keyframes particleDrift {
+  0% { transform: translate(0, 0); opacity: 0.6; }
+  50% { transform: translate(-8px, 6px); opacity: 1; }
+  100% { transform: translate(5px, -4px); opacity: 0.7; }
+}
+
+// ======================== 面板通用样式 ========================
 .panel-box {
 .panel-box {
-  background: rgba(13, 34, 71, 0.75);
-  border: 1px solid rgba(26, 58, 106, 0.6);
-  border-radius: 4px;
+  background: transparent;
+  border: 1px solid rgba(72, 180, 255, 0.12);
+  border-radius: 6px;
+  padding: 14px 16px;
   position: relative;
   position: relative;
   overflow: hidden;
   overflow: hidden;
+  transition: box-shadow 0.4s ease;
+  display: flex;
+  flex-direction: column;
 
 
-  // 旋转流光层
+  // 面板内背景遮罩(遮挡旋转锥形渐变)
   &::before {
   &::before {
     content: '';
     content: '';
     position: absolute;
     position: absolute;
+    inset: 2px;
+    background: #061638;
+    border-radius: 4px;
+    z-index: 1;
+  }
+
+  // 旋转边框流光
+  &::after {
+    content: '';
+    position: absolute;
     top: 50%;
     top: 50%;
     left: 50%;
     left: 50%;
     width: 200%;
     width: 200%;
     aspect-ratio: 1;
     aspect-ratio: 1;
     background: conic-gradient(
     background: conic-gradient(
-      transparent 0deg,
-      transparent 250deg,
-      rgba(59, 175, 214, 0.3) 265deg,
-      #3BAFD6 280deg,
-      rgba(59, 175, 214, 0.3) 295deg,
-      transparent 310deg,
-      transparent 360deg
+      from 0deg at 50% 50%,
+      #3BAFD6 0%,
+      transparent 8%,
+      transparent 92%,
+      #3BAFD6 100%
     );
     );
-    animation: panelGlow 4s linear infinite;
-    pointer-events: none;
+    transform: translate(-50%, -50%);
+    animation: borderGlowRotate 4s linear infinite;
     z-index: 0;
     z-index: 0;
+    pointer-events: none;
+    will-change: transform;
   }
   }
 
 
-  // 内部遮罩层(只露出1px边框流光)
-  &::after {
-    content: '';
-    position: absolute;
-    top: 1px;
-    left: 1px;
-    right: 1px;
-    bottom: 1px;
-    background: #0c2044;
-    border-radius: 3px;
-    pointer-events: none;
-    z-index: 0;
+  &:hover {
+    box-shadow: 0 0 24px rgba(59, 175, 214, 0.2) !important;
   }
   }
 
 
   .panel-title {
   .panel-title {
     font-size: 15px;
     font-size: 15px;
-    font-weight: bold;
-    color: $text-primary;
-    padding: 10px 14px;
-    border-bottom: 1px solid rgba(26, 58, 106, 0.4);
+    font-weight: 600;
+    letter-spacing: 2px;
+    color: #fff;
+    margin-bottom: 12px;
+    padding-left: 12px;
+    position: relative;
+    z-index: 2;
     display: flex;
     display: flex;
     align-items: center;
     align-items: center;
     flex-shrink: 0;
     flex-shrink: 0;
-    position: relative;
-    z-index: 1;
 
 
     &::before {
     &::before {
       content: '';
       content: '';
-      display: inline-block;
+      position: absolute;
+      left: 0;
+      top: 2px;
       width: 3px;
       width: 3px;
       height: 14px;
       height: 14px;
-      background: $accent-blue;
-      margin-right: 8px;
+      background: $accent;
       border-radius: 2px;
       border-radius: 2px;
+      box-shadow: 0 0 8px $accent;
+      animation: titleBarGlow 2.5s ease-in-out infinite;
     }
     }
   }
   }
 
 
   .panel-content {
   .panel-content {
-    padding: 10px 14px;
+    padding: 0;
     position: relative;
     position: relative;
-    z-index: 1;
+    z-index: 2;
+    flex: 1;
+    min-height: 0;
+    display: flex;
+    flex-direction: column;
   }
   }
 }
 }
 
 
-@keyframes panelGlow {
+// 面板四角装饰
+.corner-deco {
+  position: absolute;
+  width: 14px;
+  height: 14px;
+  border-color: $accent;
+  border-style: solid;
+  opacity: 0.7;
+  z-index: 4;
+  animation: cornerBreathe 3s ease-in-out infinite;
+
+  &.tl { top: -1px; left: -1px; border-width: 2px 0 0 2px; }
+  &.tr { top: -1px; right: -1px; border-width: 2px 2px 0 0; animation-delay: 0.5s; }
+  &.bl { bottom: -1px; left: -1px; border-width: 0 0 2px 2px; animation-delay: 1s; }
+  &.br { bottom: -1px; right: -1px; border-width: 0 2px 2px 0; animation-delay: 1.5s; }
+}
+
+@keyframes cornerBreathe {
+  0%, 100% { opacity: 0.4; box-shadow: 0 0 4px rgba(72,215,255,0.2); }
+  50% { opacity: 1; box-shadow: 0 0 12px rgba(72,215,255,0.6), 0 0 24px rgba(72,215,255,0.2); }
+}
+
+@keyframes borderGlowRotate {
   from { transform: translate(-50%, -50%) rotate(0deg); }
   from { transform: translate(-50%, -50%) rotate(0deg); }
   to { transform: translate(-50%, -50%) rotate(360deg); }
   to { transform: translate(-50%, -50%) rotate(360deg); }
 }
 }
+
+@keyframes titleBarGlow {
+  0%, 100% { box-shadow: 0 0 6px $accent; opacity: 0.7; }
+  50% { box-shadow: 0 0 14px $accent, 0 0 24px rgba(72,215,255,0.3); opacity: 1; }
+}

+ 29 - 16
src/styles/variables.scss

@@ -1,23 +1,36 @@
-// 主题色变量
-$bg-dark: #0a1a3a;
-$bg-panel: #0d2247;
-$bg-panel-inner: #0f2a55;
-$border-color: #1a3a6a;
-$border-highlight: #1e90ff;
-$text-primary: #ffffff;
-$text-secondary: #8cb8e6;
+// 主题色变量 (对齐原型 agentDemo_dashboard.html)
+$bg-0: #020c1b;
+$bg-1: #061630;
+$bg-2: #0a2550;
+$bg-dark: $bg-0;
+$bg-panel: rgba(6, 22, 56, 0.82);
+$bg-panel-inner: #061630;
+$border-color: rgba(72, 180, 255, 0.28);
+$border-highlight: #48d7ff;
+$panel-glow: rgba(72, 180, 255, 0.06);
+
+$text-primary: #d8f4ff;
+$text-secondary: #7eacc8;
 $text-muted: #5a7da8;
 $text-muted: #5a7da8;
-$accent-blue: #2196f3;
-$accent-cyan: #00e5ff;
-$accent-orange: #ff9800;
-$accent-red: #f44336;
-$accent-yellow: #ffc107;
-$accent-green: #4caf50;
-$accent-purple: #9c27b0;
+
+$accent: #48d7ff;
+$accent-blue: #3a7bff;
+$accent-cyan: #48d7ff;
+$accent-orange: #ffb020;
+$accent-red: #ff4d4f;
+$accent-yellow: #ffcc00;
+$accent-green: #36d399;
+$accent-purple: #a78bfa;
+
+// 安全等级色
+$grade-I: #ff4d4f;
+$grade-II: #ff8c00;
+$grade-III: #ffcc00;
+$grade-IV: #3a7bff;
 
 
 // 大屏尺寸
 // 大屏尺寸
 $screen-width: 1920px;
 $screen-width: 1920px;
 $screen-height: 1080px;
 $screen-height: 1080px;
 
 
 // Header 高度
 // Header 高度
-$header-height: 80px;
+$header-height: 72px;

+ 40 - 28
src/views/LabStatus.vue

@@ -2,23 +2,24 @@
   <div class="lab-status-page">
   <div class="lab-status-page">
     <ScreenHeader />
     <ScreenHeader />
     <div class="page-body">
     <div class="page-body">
-      <!-- 左列: 基本情况 + 安全分级 + 人数统计 -->
+      <!-- 左列 -->
       <div class="col col-left">
       <div class="col col-left">
         <BasicStats class="section s-basic" />
         <BasicStats class="section s-basic" />
         <SafetyLevel class="section s-safety" />
         <SafetyLevel class="section s-safety" />
         <PersonStats class="section s-person" />
         <PersonStats class="section s-person" />
       </div>
       </div>
-      <!-- 中列: 环境感知 + 风险预警 -->
+      <!-- 中列 -->
       <div class="col col-center">
       <div class="col col-center">
         <EnvSensing class="section s-env" />
         <EnvSensing class="section s-env" />
         <RiskWarning class="section s-risk" />
         <RiskWarning class="section s-risk" />
       </div>
       </div>
-      <!-- 右列: 设备统计 + 设备分类 -->
+      <!-- 右列 -->
       <div class="col col-right">
       <div class="col col-right">
         <DeviceStats class="section s-device" />
         <DeviceStats class="section s-device" />
         <EquipmentStats class="section s-equip" />
         <EquipmentStats class="section s-equip" />
       </div>
       </div>
     </div>
     </div>
+    <AlertModal ref="alertModal" />
   </div>
   </div>
 </template>
 </template>
 
 
@@ -31,6 +32,7 @@ import EnvSensing from '@/components/LabStats/EnvSensing.vue'
 import RiskWarning from '@/components/LabStats/RiskWarning.vue'
 import RiskWarning from '@/components/LabStats/RiskWarning.vue'
 import DeviceStats from '@/components/LabStats/DeviceStats.vue'
 import DeviceStats from '@/components/LabStats/DeviceStats.vue'
 import EquipmentStats from '@/components/LabStats/EquipmentStats.vue'
 import EquipmentStats from '@/components/LabStats/EquipmentStats.vue'
+import AlertModal from '@/components/AlertModal.vue'
 
 
 export default {
 export default {
   name: 'LabStatus',
   name: 'LabStatus',
@@ -42,7 +44,25 @@ export default {
     EnvSensing,
     EnvSensing,
     RiskWarning,
     RiskWarning,
     DeviceStats,
     DeviceStats,
-    EquipmentStats
+    EquipmentStats,
+    AlertModal
+  },
+  mounted() {
+    // 15秒后演示触发预警弹窗
+    this.alertTimer = setTimeout(() => {
+      this.$refs.alertModal.show({
+        lab: '化学实验室A101(A101)',
+        building: '科研楼A-1层',
+        unit: '化学品安全研究所',
+        param: 'TVOC 超标',
+        value: '1.82 mg/m³',
+        person: '张明',
+        phone: '138-0012-3456'
+      })
+    }, 15000)
+  },
+  beforeDestroy() {
+    clearTimeout(this.alertTimer)
   }
   }
 }
 }
 </script>
 </script>
@@ -51,54 +71,46 @@ export default {
 .lab-status-page {
 .lab-status-page {
   width: 1920px;
   width: 1920px;
   height: 1080px;
   height: 1080px;
-  background: $bg-dark;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   overflow: hidden;
   overflow: hidden;
+  position: relative;
 }
 }
 
 
 .page-body {
 .page-body {
   flex: 1;
   flex: 1;
   display: flex;
   display: flex;
-  gap: 12px;
-  padding: 12px 16px;
+  gap: 14px;
+  padding: 14px;
   overflow: hidden;
   overflow: hidden;
 }
 }
 
 
 .col {
 .col {
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
-  gap: 12px;
+  gap: 14px;
 }
 }
 
 
-// 左列 ~28%
-.col-left {
-  width: 520px;
+.col-left, .col-right {
+  width: 440px;
   flex-shrink: 0;
   flex-shrink: 0;
 }
 }
 
 
-// 中列 ~40%
 .col-center {
 .col-center {
   flex: 1;
   flex: 1;
   min-width: 0;
   min-width: 0;
 }
 }
 
 
-// 右列 ~30%
-.col-right {
-  width: 440px;
-  flex-shrink: 0;
-}
-
-// 左列3个模块比例: 基本情况 ~30%, 安全分级 ~38%, 人数走势 ~32%
-.s-basic { flex: 3; }
-.s-safety { flex: 3.8; }
-.s-person { flex: 3.2; }
+// 左列: 基本情况 260px固定, 分级flex:1, 人数260px固定
+.s-basic { height: 260px; flex-shrink: 0; }
+.s-safety { flex: 1; }
+.s-person { height: 260px; flex-shrink: 0; }
 
 
-// 中列2个模块比例: 环境感知 ~65%, 风险预警 ~35%
-.s-env { flex: 6.5; }
-.s-risk { flex: 3.5; }
+// 中列: 环境感知flex:1, 风险预警320px固定
+.s-env { flex: 1; }
+.s-risk { height: 320px; flex-shrink: 0; }
 
 
-// 右列2个模块比例: 设备统计 ~48%, 设备分类 ~52%
-.s-device { flex: 4.8; }
-.s-equip { flex: 5.2; }
+// 右列: 设备统计和设备分类各一半
+.s-device { flex: 1; }
+.s-equip { flex: 1; }
 </style>
 </style>

+ 101 - 13
src/views/VideoMonitor.vue

@@ -2,10 +2,11 @@
   <div class="video-monitor-page">
   <div class="video-monitor-page">
     <ScreenHeader />
     <ScreenHeader />
     <div class="page-body">
     <div class="page-body">
-      <div class="side-panel">
+      <div class="video-left">
         <BuildingNav @node-click="handleNodeClick" />
         <BuildingNav @node-click="handleNodeClick" />
       </div>
       </div>
-      <div class="main-panel">
+      <div class="video-right">
+        <div class="video-breadcrumb">{{ breadcrumb }}</div>
         <VideoGrid />
         <VideoGrid />
       </div>
       </div>
     </div>
     </div>
@@ -19,14 +20,14 @@ import VideoGrid from '@/components/VideoMonitor/VideoGrid.vue'
 
 
 export default {
 export default {
   name: 'VideoMonitor',
   name: 'VideoMonitor',
-  components: {
-    ScreenHeader,
-    BuildingNav,
-    VideoGrid
+  components: { ScreenHeader, BuildingNav, VideoGrid },
+  data() {
+    return {
+      breadcrumb: '安科院院区 → 科研楼A → 3层'
+    }
   },
   },
   methods: {
   methods: {
     handleNodeClick(data) {
     handleNodeClick(data) {
-      // 节点点击事件,后期可用于筛选视频
       console.log('选中节点:', data.label)
       console.log('选中节点:', data.label)
     }
     }
   }
   }
@@ -37,7 +38,6 @@ export default {
 .video-monitor-page {
 .video-monitor-page {
   width: 1920px;
   width: 1920px;
   height: 1080px;
   height: 1080px;
-  background: $bg-dark;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   overflow: hidden;
   overflow: hidden;
@@ -46,18 +46,106 @@ export default {
 .page-body {
 .page-body {
   flex: 1;
   flex: 1;
   display: flex;
   display: flex;
-  gap: 16px;
-  padding: 16px 20px;
+  gap: 14px;
+  padding: 14px;
   overflow: hidden;
   overflow: hidden;
 }
 }
 
 
-.side-panel {
-  width: 280px;
+.video-left {
+  width: 300px;
   flex-shrink: 0;
   flex-shrink: 0;
+  position: relative;
+
+  &::after {
+    content: '';
+    position: absolute;
+    left: 0;
+    top: -100%;
+    width: 1px;
+    height: 60%;
+    background: linear-gradient(180deg, transparent, rgba(72,215,255,0.7), rgba(72,215,255,0.9), rgba(72,215,255,0.7), transparent);
+    animation: sidebarScan 5s linear infinite;
+    pointer-events: none;
+    z-index: 3;
+  }
+
+  &::before {
+    content: '';
+    position: absolute;
+    left: 0;
+    top: 10%;
+    bottom: 10%;
+    width: 2px;
+    background: linear-gradient(180deg, transparent, $accent, transparent);
+    opacity: 0.4;
+    animation: sidebarEdgeGlow 3s ease-in-out infinite;
+    z-index: 3;
+    pointer-events: none;
+  }
 }
 }
 
 
-.main-panel {
+@keyframes sidebarScan {
+  0% { top: -60%; }
+  100% { top: 160%; }
+}
+
+@keyframes sidebarEdgeGlow {
+  0%, 100% { opacity: 0.25; }
+  50% { opacity: 0.7; box-shadow: 0 0 12px $accent; }
+}
+
+.video-right {
   flex: 1;
   flex: 1;
+  display: flex;
+  flex-direction: column;
   min-width: 0;
   min-width: 0;
+  position: relative;
+
+  &::before {
+    content: '';
+    position: absolute;
+    top: 0; left: 0; right: 0; bottom: 0;
+    pointer-events: none;
+    z-index: 0;
+    background-image:
+      radial-gradient(1px 1px at 10% 20%, rgba(72,215,255,0.3) 50%, transparent 100%),
+      radial-gradient(1px 1px at 75% 80%, rgba(72,215,255,0.3) 50%, transparent 100%),
+      radial-gradient(1px 1px at 90% 40%, rgba(72,215,255,0.25) 50%, transparent 100%);
+    animation: videoParticles 12s ease-in-out infinite alternate;
+  }
+}
+
+@keyframes videoParticles {
+  0% { opacity: 0.3; }
+  50% { opacity: 0.7; }
+  100% { opacity: 0.3; transform: translateY(-10px); }
+}
+
+.video-breadcrumb {
+  font-size: 14px;
+  color: $text-secondary;
+  margin-bottom: 10px;
+  padding: 4px 0;
+  border-bottom: 1px solid rgba(72,215,255,0.1);
+  position: relative;
+  z-index: 1;
+  overflow: hidden;
+
+  &::before {
+    content: '';
+    position: absolute;
+    bottom: 0;
+    left: -80px;
+    width: 80px;
+    height: 1px;
+    background: linear-gradient(90deg, transparent, $accent, transparent);
+    animation: breadcrumbFlow 5s linear infinite;
+    pointer-events: none;
+  }
+}
+
+@keyframes breadcrumbFlow {
+  0% { left: -80px; }
+  100% { left: calc(100% + 80px); }
 }
 }
 </style>
 </style>