Przeglądaj źródła

初始化aigc仓库及产品ui

anne 1 tydzień temu
rodzic
commit
2280697499
50 zmienionych plików z 1221 dodań i 12043 usunięć
  1. 0 5
      babel.config.js
  2. 0 101
      call-word/play.md
  3. 0 1
      dist/css/515.286b12d8.css
  4. 0 1
      dist/css/553.6898c014.css
  5. 0 1
      dist/css/app.397d4214.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 2
      dist/js/515.40e467b6.js
  11. 0 1
      dist/js/515.40e467b6.js.map
  12. 0 12
      dist/js/553.7a41c852.js
  13. 0 1
      dist/js/553.7a41c852.js.map
  14. 0 2
      dist/js/app.16ae2cba.js
  15. 0 1
      dist/js/app.16ae2cba.js.map
  16. 0 55
      dist/js/chunk-vendors.db81f0fc.js
  17. 0 1
      dist/js/chunk-vendors.db81f0fc.js.map
  18. 0 0
      img/design-preview-half.png
  19. 0 7459
      package-lock.json
  20. 0 33
      package.json
  21. 116 0
      prompt/超大屏prompt.md
  22. BIN
      prototype-ui/baseBg.png
  23. 83 55
      call-word/index-v2.html
  24. 38 0
      prototype-ui/modules.json
  25. 930 0
      prototype-ui/package-lock.json
  26. 5 0
      prototype-ui/package.json
  27. 49 0
      prototype-ui/screenshot.js
  28. 0 16
      public/index.html
  29. 0 18
      src/App.vue
  30. 0 27
      src/api/auth.js
  31. 0 220
      src/api/screen.js
  32. 0 243
      src/components/AlarmInfo.vue
  33. 0 144
      src/components/CategoryChart.vue
  34. 0 330
      src/components/EnvMonitorStats.vue
  35. 0 403
      src/components/EquipmentStats.vue
  36. 0 570
      src/components/EventStats.vue
  37. 0 310
      src/components/LabEnvironment.vue
  38. 0 285
      src/components/PersonnelTrend.vue
  39. 0 165
      src/components/SafetyCompliance.vue
  40. 0 278
      src/components/ScreenHeader.vue
  41. 0 593
      src/components/SecurityMonitor.vue
  42. 0 21
      src/main.js
  43. 0 40
      src/router/index.js
  44. 0 31
      src/store/index.js
  45. 0 161
      src/styles/global.scss
  46. 0 37
      src/styles/variables.scss
  47. 0 33
      src/utils/request.js
  48. 0 238
      src/views/Login.vue
  49. 0 130
      src/views/Screen.vue
  50. 0 17
      vue.config.js

+ 0 - 5
babel.config.js

@@ -1,5 +0,0 @@
-module.exports = {
-  presets: [
-    '@vue/cli-plugin-babel/preset'
-  ]
-}

+ 0 - 101
call-word/play.md

@@ -1,101 +0,0 @@
-# 项目:大屏数据展示(Vue2)
-
-## 一、项目基础信息
-- **项目名称**:大屏数据展示
-- **技术栈**:
-  - 框架:Vue 2.7.x
-  - 构建工具:@vue/cli 4/5
-  - UI组件库:Element UI(完整引入)
-  - 状态管理:Vuex 3.x
-  - 路由:Vue Router 3.x
-  - HTTP请求:axios
-  - 图表:ECharts 5
-  - 工具库:js-md5(用于密码加密)
-  - CSS预处理器:Sass(使用 `node-sass` 或 `sass`)
-  - 包管理器:npm
-- **Node版本要求**:>= 14.0
-
-## 二、核心功能需求
-- 构建一个典型的大屏数据展示系统,分辨率尺寸为9600*2800px
-- 包含以下完整页面:
-
-### 1. 登录页(Login)
-- 表单:用户名/手机号、密码(密码使用 `js-md5` 加密后模拟提交)
-- 验证码(可选,可用模拟验证码 0000)
-- 登录按钮,点击后调用模拟登录API(使用 `axios`,可先mock数据)
-- 登录成功后保存token(使用Vuex存储)并跳转到首页
-- 该页面样式你自行发挥 主要配色与UI图相近即可
-
-### 2. 大屏页面
-- 该页面应由多个组件组成 
-- 如 实验室事件情况统计 为一个组件 实实验室安全合格统计为一个组件 如此类推
-- 页面样式严格按照 UI图 样式 项目基础尺寸严格按照9600*2800px制作
-
-## 三、设计风格与规范
-- 样式:统一使用Sass编写自定义样式,变量文件统一管理
-
-## 四、执行计划
-
-### 步骤1:初始化项目骨架
-- 使用 `vue create` 创建项目,选择以下特性:
-  - Babel
-  - Router(使用history模式)
-  - Vuex
-  - CSS Pre-processors(选择Sass/SCSS)
-- 删除默认的HelloWorld组件,清理App.vue
-- 安装依赖:`element-ui`、`axios`、`echarts`、`js-md5`
-- 在 `main.js` 中完整引入Element UI和样式
-- 创建基本目录结构:
-src/
-api/ # axios接口封装
-assets/ # 静态资源
-components/ # 公共组件
-router/ # 路由配置
-store/ # Vuex模块
-styles/ # 全局样式
-utils/ # 工具函数
-views/ # 页面视图
-
-- 配置 `vue.config.js`(如有必要,可设置别名等)
-
-
-### 步骤2:配置路由和基础布局
-- 在 `router/index.js` 中配置登录页(/login)
-- 在 `App.vue` 中放置路由视图
-
-### 步骤3:实现页面
-- 实现 大屏页面
-- 页面中的视频相关功能不用实现 后期我来处理 只需提供基础box与样式即可
-## 五、附加说明
-- 所有API请求先在 `src/api` 中用 `setTimeout` 模拟返回数据,便于独立开发
-- 代码风格使用函数式组件(Vue2 Options API即可)
-- 注释:复杂逻辑(如图表初始化、加密)需添加注释
-
-**开始执行吧**
-
-# 修改页面
-- 观察UI图 调整4个大区域的比例
-
-# 修改页面
-- 调整4个区域比例 从左至右 第一个区域占比20% 第二个区域占比15% 第三个区域占比45% 第四个区域占比20%
-
-# 修改页面-解析 call-word\index-v2.html 文件
-1. 解析文件 把页面中的内容拆分到我的项目中替换原有组件
-
-# 读取新的UI图
-- @call-word/UI.png
-- 检查重构的页面是否一致
-
-# 排版布局错误 修改组件尺寸
-1. 左侧第一个列 从上到下的组件应是
-- 实验室基本情况统计 宽1892 高956
-- 实验室安全分级统计 宽1892 高1017
-- 实验室进入人数统计及走势 宽1892 高591
-2. 左侧第二个列 从上到下的组件应是
-- 智能环境感知应用设备统计 宽1414 高666
-- 实验室设备分类及使用统计 宽1414 高1916
-3. 左侧第三个列
-- 实时监控  宽4282 高2600
-3. 左侧第四个列 从上到下的组件应是
-- 实验环境安全智能感知 宽1892 高1807
-- 实验室实时风险预警 宽1892 高775

Plik diff jest za duży
+ 0 - 1
dist/css/515.286b12d8.css


Plik diff jest za duży
+ 0 - 1
dist/css/553.6898c014.css


Plik diff jest za duży
+ 0 - 1
dist/css/app.397d4214.css


Plik diff jest za duży
+ 0 - 1
dist/css/chunk-vendors.10dd4e95.css


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


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


Plik diff jest za duży
+ 0 - 1
dist/index.html


Plik diff jest za duży
+ 0 - 2
dist/js/515.40e467b6.js


Plik diff jest za duży
+ 0 - 1
dist/js/515.40e467b6.js.map


Plik diff jest za duży
+ 0 - 12
dist/js/553.7a41c852.js


Plik diff jest za duży
+ 0 - 1
dist/js/553.7a41c852.js.map


Plik diff jest za duży
+ 0 - 2
dist/js/app.16ae2cba.js


Plik diff jest za duży
+ 0 - 1
dist/js/app.16ae2cba.js.map


Plik diff jest za duży
+ 0 - 55
dist/js/chunk-vendors.db81f0fc.js


Plik diff jest za duży
+ 0 - 1
dist/js/chunk-vendors.db81f0fc.js.map


call-word/UI.png → img/design-preview-half.png


Plik diff jest za duży
+ 0 - 7459
package-lock.json


+ 0 - 33
package.json

@@ -1,33 +0,0 @@
-{
-  "name": "lab-safety-screen",
-  "version": "1.0.0",
-  "private": true,
-  "scripts": {
-    "serve": "vue-cli-service serve",
-    "build": "vue-cli-service build"
-  },
-  "dependencies": {
-    "axios": "^0.27.2",
-    "core-js": "^3.8.3",
-    "echarts": "^5.4.3",
-    "element-ui": "^2.15.14",
-    "js-md5": "^0.8.3",
-    "vue": "^2.7.16",
-    "vue-router": "^3.6.5",
-    "vuex": "^3.6.2"
-  },
-  "devDependencies": {
-    "@vue/cli-plugin-babel": "~5.0.8",
-    "@vue/cli-plugin-router": "~5.0.8",
-    "@vue/cli-plugin-vuex": "~5.0.8",
-    "@vue/cli-service": "~5.0.8",
-    "sass": "~1.32.13",
-    "sass-loader": "^12.0.0",
-    "vue-template-compiler": "^2.7.16"
-  },
-  "browserslist": [
-    "> 1%",
-    "last 2 versions",
-    "not dead"
-  ]
-}

Plik diff jest za duży
+ 116 - 0
prompt/超大屏prompt.md


BIN
prototype-ui/baseBg.png


+ 83 - 55
call-word/index-v2.html

@@ -489,7 +489,8 @@ html, body { width:100%; height:100%; overflow:auto; background:var(--bg-deep);
 #chart-line  { width:100%; height:100%; }
 #chart-gauge { width:100%; height:100%; }
 #chart-ring  { width:100%; height:100%; }
-#chart-pie   { width:100%; height:100%; }
+#chart-pie-status { width:100%; flex:1; min-height:0; }
+#chart-pie-usage  { width:100%; flex:1; min-height:0; }
 </style>
 </head>
 <body>
@@ -747,40 +748,53 @@ html, body { width:100%; height:100%; overflow:auto; background:var(--bg-deep);
           </div>
           <div style="display:flex;gap:20px;align-items:center">
             <div id="chart-gauge" style="flex:0 0 295px;height:295px"></div>
-            <div class="device-list-row" style="flex:1">
-              <div class="device-list-item"><div class="dli-icon">🏷️</div><div class="dli-num">86</div><div class="dli-name">电子信息铭牌</div></div>
-              <div class="device-list-item"><div class="dli-icon">⚗️</div><div class="dli-num">44</div><div class="dli-name">化学品智能终端</div></div>
-              <div class="device-list-item"><div class="dli-icon">🌡️</div><div class="dli-num">128</div><div class="dli-name">传感器套件</div></div>
-              <div class="device-list-item"><div class="dli-icon">📷</div><div class="dli-num">72</div><div class="dli-name">智能摄像设备</div></div>
+            <div style="flex:1;display:flex;flex-direction:column;gap:12px">
+              <div style="display:flex;gap:12px">
+                <div class="device-list-item" style="flex:1"><div class="dli-icon">🏷️</div><div class="dli-num">86</div><div class="dli-name">电子信息铭牌</div></div>
+                <div class="device-list-item" style="flex:1"><div class="dli-icon">⚗️</div><div class="dli-num">44</div><div class="dli-name">化学品智能终端</div></div>
+                <div class="device-list-item" style="flex:1"><div class="dli-icon">🌡️</div><div class="dli-num">128</div><div class="dli-name">传感器</div></div>
+              </div>
+              <div style="display:flex;gap:12px;justify-content:center">
+                <div class="device-list-item" style="flex:0 0 calc(33.33% - 6px)"><div class="dli-icon">📷</div><div class="dli-num">72</div><div class="dli-name">智慧摄像头</div></div>
+                <div class="device-list-item" style="flex:0 0 calc(33.33% - 6px)"><div class="dli-icon">📡</div><div class="dli-num">35</div><div class="dli-name">其他智能设备</div></div>
+              </div>
             </div>
           </div>
         </div>
       </div>
 
-      <!-- 实验室设备分类及使用统计 (4:2:4) -->
+      <!-- 实验室设备分类及使用统计 (4:6) -->
       <div class="panel" style="flex:1;min-height:650px;display:flex;flex-direction:column">
         <div class="border-beam"></div>
         <div class="panel-header">
           <div class="panel-header-icon">🔬</div>
           <span class="panel-title">实验室设备分类及使用统计</span>
         </div>
-        <!-- 4:2:4 layout -->
+        <!-- 4:6 layout -->
         <div style="flex:1;display:flex;flex-direction:column;padding:15px 25px;gap:12px;min-height:0">
-          <!-- 上: 占4 环形图 - 设备分类 -->
+          <!-- 上: 占4 环形图 - 设备分类(含图例:名称/数量/占比) -->
           <div style="flex:4;min-height:0">
             <div id="chart-ring" style="height:100%;width:100%"></div>
           </div>
-          <!-- 中: 占2 统计数字 -->
-          <div style="flex:2;min-height:0;display:flex;align-items:center">
-            <div class="equip-mid-row" style="width:100%">
+          <!-- 下: 占6 统计 + 双饼图 -->
+          <div style="flex:6;min-height:0;display:flex;flex-direction:column;gap:10px">
+            <!-- 上方 3网格统计 -->
+            <div class="equip-mid-row" style="flex:0 0 auto">
               <div class="equip-stat-item"><div class="ev">2,458</div><div class="el">设备总数(台)</div></div>
               <div class="equip-stat-item"><div class="ev">18,620</div><div class="el">使用时长(h)</div></div>
               <div class="equip-stat-item"><div class="ev">62.4%</div><div class="el">设备使用率</div></div>
             </div>
-          </div>
-          <!-- 下: 占4 饼图 - 使用状态 -->
-          <div style="flex:4;min-height:0">
-            <div id="chart-pie" style="height:100%;width:100%"></div>
+            <!-- 下方 左右双饼图 -->
+            <div style="flex:1;min-height:0;display:flex;gap:12px">
+              <div style="flex:1;min-height:0;display:flex;flex-direction:column;gap:6px">
+                <div style="text-align:center;margin-top:100px;padding:5px 0 7px;font-size:22px;font-weight:600;letter-spacing:3px;color:var(--cyan);border-bottom:1px solid rgba(0,216,255,0.22);text-shadow:0 0 10px rgba(0,216,255,0.45)">设备状态统计</div>
+                <div id="chart-pie-status" style="flex:1;min-height:0"></div>
+              </div>
+              <div style="flex:1;min-height:0;display:flex;flex-direction:column;gap:6px">
+                <div style="text-align:center;margin-top:100px;padding:5px 0 7px;font-size:22px;font-weight:600;letter-spacing:3px;color:var(--cyan);border-bottom:1px solid rgba(0,216,255,0.22);text-shadow:0 0 10px rgba(0,216,255,0.45)">使用状态统计</div>
+                <div id="chart-pie-usage" style="flex:1;min-height:0"></div>
+              </div>
+            </div>
           </div>
         </div>
       </div>
@@ -1230,22 +1244,28 @@ function initCharts() {
       {value:240, name:'辅助设备', itemStyle:{color:'#00e5c8', shadowBlur:8, shadowColor:'rgba(0,229,200,0.5)'}},
       {value:358, name:'其他',     itemStyle:{color:'#f97316', shadowBlur:8, shadowColor:'rgba(249,115,22,0.5)'}}
     ];
-    const ringCountMap = {检测设备:680, 分析仪器:520, 制备设备:380, 安全设备:280, 辅助设备:240, 其他:358};
+    const total = ringData.reduce((s,d)=>s+d.value,0);
+    const ringCountMap = Object.fromEntries(ringData.map(d=>[d.name,d.value]));
     c.setOption({
       backgroundColor:'transparent',
       tooltip:{ trigger:'item', formatter:'{b}: {c}台 ({d}%)', ...TOOLTIP_CFG },
       legend:{
         orient:'vertical', right:'1%', top:'middle',
-        icon:'circle', itemWidth:28, itemHeight:28, itemGap:22,
-        textStyle:{ color:'#a8cce8', fontSize:22 },
-        formatter: name => `{nm|${name}}  {vl|${ringCountMap[name]}台}`,
+        icon:'circle', itemWidth:24, itemHeight:24, itemGap:18,
+        textStyle:{ color:'#a8cce8', fontSize:20 },
+        formatter: name => {
+          const v = ringCountMap[name];
+          const pct = (v/total*100).toFixed(1);
+          return `{nm|${name}}  {vl|${v}台}  {pt|${pct}%}`;
+        },
         rich:{
-          nm:{ fontSize:22, color:'#a8cce8', width:90 },
-          vl:{ fontSize:26, fontWeight:700, color:'#fff' }
+          nm:{ fontSize:20, color:'#a8cce8', width:80 },
+          vl:{ fontSize:23, fontWeight:700, color:'#fff', width:70 },
+          pt:{ fontSize:20, color:'#ffd740', width:55 }
         }
       },
       series:[{
-        type:'pie', radius:['38%','62%'], center:['36%','50%'],
+        type:'pie', radius:['38%','62%'], center:['32%','50%'],
         itemStyle:{ borderRadius:4, borderColor:'rgba(3,14,31,0.5)', borderWidth:2 },
         label:{ show:true, formatter:'{c}台', fontSize:20, color:'#a8cce8' },
         labelLine:{ length:10, length2:10, lineStyle:{color:'rgba(30,144,255,0.4)', width:2} },
@@ -1255,44 +1275,52 @@ function initCharts() {
     });
   }
 
-  // --- 饼图: 设备使用状态 (2区设备统计下部) ---
-  const ppEl = document.getElementById('chart-pie');
-  if (ppEl) {
-    const c = echarts.init(ppEl, null, {renderer:'canvas', devicePixelRatio:2});
-    const pieData = [
+  // --- 饼图: 设备状态(正常/维修/报废) ---
+  const psEl = document.getElementById('chart-pie-status');
+  if (psEl) {
+    const c = echarts.init(psEl, null, {renderer:'canvas', devicePixelRatio:2});
+    const statusData = [
+      {value:2180, name:'正常', itemStyle:{color:'#00e676', shadowBlur:10, shadowColor:'rgba(0,230,118,0.5)'}},
+      {value:198,  name:'维修', itemStyle:{color:'#f59e0b', shadowBlur:10, shadowColor:'rgba(245,158,11,0.5)'}},
+      {value:80,   name:'报废', itemStyle:{color:'#ef4444', shadowBlur:10, shadowColor:'rgba(239,68,68,0.5)'}}
+    ];
+    const smMap = Object.fromEntries(statusData.map(d=>[d.name,d.value]));
+    c.setOption({
+      backgroundColor:'transparent',
+      tooltip:{ trigger:'item', formatter:'{b}: {c}台 ({d}%)', ...TOOLTIP_CFG },
+      legend:{ show:false },
+      series:[{
+        type:'pie', radius:['35%','58%'], center:['50%','55%'],
+        itemStyle:{ borderRadius:5, borderColor:'rgba(3,14,31,0.5)', borderWidth:2 },
+        label:{ show:true, formatter:'{b}\n{c}台\n{d}%', fontSize:20, color:'#a8cce8', lineHeight:30 },
+        labelLine:{ length:14, length2:10, lineStyle:{color:'rgba(30,144,255,0.4)', width:2} },
+        data: statusData,
+        emphasis:{ scale:true, scaleSize:5, itemStyle:{ shadowBlur:20, shadowColor:'rgba(30,144,255,0.6)' } }
+      }]
+    });
+  }
+
+  // --- 饼图: 使用状态(使用/空闲/维护) ---
+  const puEl = document.getElementById('chart-pie-usage');
+  if (puEl) {
+    const c = echarts.init(puEl, null, {renderer:'canvas', devicePixelRatio:2});
+    const usageData = [
       {value:486,  name:'使用', itemStyle:{color:'#1e90ff', shadowBlur:10, shadowColor:'rgba(30,144,255,0.5)'}},
-      {value:1840, name:'空闲', itemStyle:{color:'#00e676', shadowBlur:10, shadowColor:'rgba(0,230,118,0.5)'}},
-      {value:34,   name:'维修', itemStyle:{color:'#f59e0b', shadowBlur:10, shadowColor:'rgba(245,158,11,0.5)'}}
+      {value:1840, name:'空闲', itemStyle:{color:'#00d8ff', shadowBlur:10, shadowColor:'rgba(0,216,255,0.5)'}},
+      {value:132,  name:'维护', itemStyle:{color:'#f97316', shadowBlur:10, shadowColor:'rgba(249,115,22,0.5)'}}
     ];
-    const countMap = {使用:486, 空闲:1840, 维修:34};
+    const umMap = Object.fromEntries(usageData.map(d=>[d.name,d.value]));
     c.setOption({
       backgroundColor:'transparent',
       tooltip:{ trigger:'item', formatter:'{b}: {c}台 ({d}%)', ...TOOLTIP_CFG },
-      legend:{
-        show:true, orient:'vertical', right:'2%', top:'middle',
-        icon:'circle', itemWidth:36, itemHeight:36, itemGap:40,
-        textStyle:{ color:'#a8cce8', fontSize:28 },
-        formatter: name => `{nm|${name}}  {vl|${countMap[name]}台}`,
-        rich:{
-          nm:{ fontSize:28, color:'#a8cce8', width:90 },
-          vl:{ fontSize:34, fontWeight:700, color:'#fff' }
-        }
-      },
+      legend:{ show:false },
       series:[{
-        type:'pie', radius:['36%','62%'], center:['38%','52%'],
+        type:'pie', radius:['35%','58%'], center:['50%','55%'],
         itemStyle:{ borderRadius:5, borderColor:'rgba(3,14,31,0.5)', borderWidth:2 },
-        label:{
-          show:true,
-          formatter: '{b}\n{c}台',
-          fontSize:24, color:'#a8cce8', lineHeight:36,
-          distanceToLabelLine:6
-        },
-        labelLine:{ length:18, length2:14, lineStyle:{color:'rgba(30,144,255,0.4)', width:2} },
-        data: pieData,
-        emphasis:{
-          scale:true, scaleSize:6,
-          itemStyle:{ shadowBlur:25, shadowColor:'rgba(30,144,255,0.6)' }
-        }
+        label:{ show:true, formatter:'{b}\n{c}台\n{d}%', fontSize:20, color:'#a8cce8', lineHeight:30 },
+        labelLine:{ length:14, length2:10, lineStyle:{color:'rgba(30,144,255,0.4)', width:2} },
+        data: usageData,
+        emphasis:{ scale:true, scaleSize:5, itemStyle:{ shadowBlur:20, shadowColor:'rgba(30,144,255,0.6)' } }
       }]
     });
   }
@@ -1410,7 +1438,7 @@ function drawFakeCam(canvas, idx) {
 // RESIZE CHARTS
 // ====================================================
 window.addEventListener('resize', () => {
-  ['chart-donut','chart-stack','chart-line','chart-gauge','chart-ring','chart-pie'].forEach(id => {
+  ['chart-donut','chart-stack','chart-line','chart-gauge','chart-ring','chart-pie-status','chart-pie-usage'].forEach(id => {
     const el = document.getElementById(id);
     if (el) { const c = echarts.getInstanceByDom(el); if (c) c.resize(); }
   });

Plik diff jest za duży
+ 38 - 0
prototype-ui/modules.json


+ 930 - 0
prototype-ui/package-lock.json

@@ -0,0 +1,930 @@
+{
+  "name": "lab-safety-monitor",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "dependencies": {
+        "puppeteer-core": "^24.38.0"
+      }
+    },
+    "node_modules/@puppeteer/browsers": {
+      "version": "2.13.0",
+      "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz",
+      "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "debug": "^4.4.3",
+        "extract-zip": "^2.0.1",
+        "progress": "^2.0.3",
+        "proxy-agent": "^6.5.0",
+        "semver": "^7.7.4",
+        "tar-fs": "^3.1.1",
+        "yargs": "^17.7.2"
+      },
+      "bin": {
+        "browsers": "lib/cjs/main-cli.js"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@tootallnate/quickjs-emscripten": {
+      "version": "0.23.0",
+      "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
+      "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
+      "license": "MIT"
+    },
+    "node_modules/@types/node": {
+      "version": "25.3.5",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz",
+      "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "undici-types": "~7.18.0"
+      }
+    },
+    "node_modules/@types/yauzl": {
+      "version": "2.10.3",
+      "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
+      "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/ast-types": {
+      "version": "0.13.4",
+      "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
+      "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/b4a": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz",
+      "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==",
+      "license": "Apache-2.0",
+      "peerDependencies": {
+        "react-native-b4a": "*"
+      },
+      "peerDependenciesMeta": {
+        "react-native-b4a": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/bare-events": {
+      "version": "2.8.2",
+      "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
+      "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
+      "license": "Apache-2.0",
+      "peerDependencies": {
+        "bare-abort-controller": "*"
+      },
+      "peerDependenciesMeta": {
+        "bare-abort-controller": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/bare-fs": {
+      "version": "4.5.5",
+      "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz",
+      "integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "bare-events": "^2.5.4",
+        "bare-path": "^3.0.0",
+        "bare-stream": "^2.6.4",
+        "bare-url": "^2.2.2",
+        "fast-fifo": "^1.3.2"
+      },
+      "engines": {
+        "bare": ">=1.16.0"
+      },
+      "peerDependencies": {
+        "bare-buffer": "*"
+      },
+      "peerDependenciesMeta": {
+        "bare-buffer": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/bare-os": {
+      "version": "3.7.1",
+      "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.7.1.tgz",
+      "integrity": "sha512-ebvMaS5BgZKmJlvuWh14dg9rbUI84QeV3WlWn6Ph6lFI8jJoh7ADtVTyD2c93euwbe+zgi0DVrl4YmqXeM9aIA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "bare": ">=1.14.0"
+      }
+    },
+    "node_modules/bare-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
+      "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "bare-os": "^3.0.1"
+      }
+    },
+    "node_modules/bare-stream": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.0.tgz",
+      "integrity": "sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "streamx": "^2.21.0",
+        "teex": "^1.0.1"
+      },
+      "peerDependencies": {
+        "bare-buffer": "*",
+        "bare-events": "*"
+      },
+      "peerDependenciesMeta": {
+        "bare-buffer": {
+          "optional": true
+        },
+        "bare-events": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/bare-url": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
+      "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "bare-path": "^3.0.0"
+      }
+    },
+    "node_modules/basic-ftp": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz",
+      "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/buffer-crc32": {
+      "version": "0.2.13",
+      "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+      "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/chromium-bidi": {
+      "version": "14.0.0",
+      "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz",
+      "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "mitt": "^3.0.1",
+        "zod": "^3.24.1"
+      },
+      "peerDependencies": {
+        "devtools-protocol": "*"
+      }
+    },
+    "node_modules/cliui": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+      "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.1",
+        "wrap-ansi": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "license": "MIT"
+    },
+    "node_modules/data-uri-to-buffer": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
+      "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/degenerator": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
+      "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ast-types": "^0.13.4",
+        "escodegen": "^2.1.0",
+        "esprima": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/devtools-protocol": {
+      "version": "0.0.1581282",
+      "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz",
+      "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==",
+      "license": "BSD-3-Clause",
+      "peer": true
+    },
+    "node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
+    "node_modules/end-of-stream": {
+      "version": "1.4.5",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+      "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+      "license": "MIT",
+      "dependencies": {
+        "once": "^1.4.0"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/escodegen": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
+      "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "esprima": "^4.0.1",
+        "estraverse": "^5.2.0",
+        "esutils": "^2.0.2"
+      },
+      "bin": {
+        "escodegen": "bin/escodegen.js",
+        "esgenerate": "bin/esgenerate.js"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "optionalDependencies": {
+        "source-map": "~0.6.1"
+      }
+    },
+    "node_modules/esprima": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+      "license": "BSD-2-Clause",
+      "bin": {
+        "esparse": "bin/esparse.js",
+        "esvalidate": "bin/esvalidate.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/events-universal": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
+      "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "bare-events": "^2.7.0"
+      }
+    },
+    "node_modules/extract-zip": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
+      "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "debug": "^4.1.1",
+        "get-stream": "^5.1.0",
+        "yauzl": "^2.10.0"
+      },
+      "bin": {
+        "extract-zip": "cli.js"
+      },
+      "engines": {
+        "node": ">= 10.17.0"
+      },
+      "optionalDependencies": {
+        "@types/yauzl": "^2.9.1"
+      }
+    },
+    "node_modules/fast-fifo": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
+      "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
+      "license": "MIT"
+    },
+    "node_modules/fd-slicer": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+      "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
+      "license": "MIT",
+      "dependencies": {
+        "pend": "~1.2.0"
+      }
+    },
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "license": "ISC",
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
+    },
+    "node_modules/get-stream": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+      "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+      "license": "MIT",
+      "dependencies": {
+        "pump": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/get-uri": {
+      "version": "6.0.5",
+      "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
+      "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==",
+      "license": "MIT",
+      "dependencies": {
+        "basic-ftp": "^5.0.2",
+        "data-uri-to-buffer": "^6.0.2",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/http-proxy-agent": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+      "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.0",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/ip-address": {
+      "version": "10.1.0",
+      "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
+      "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/lru-cache": {
+      "version": "7.18.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+      "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/mitt": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+      "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+      "license": "MIT"
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/netmask": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
+      "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/pac-proxy-agent": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
+      "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==",
+      "license": "MIT",
+      "dependencies": {
+        "@tootallnate/quickjs-emscripten": "^0.23.0",
+        "agent-base": "^7.1.2",
+        "debug": "^4.3.4",
+        "get-uri": "^6.0.1",
+        "http-proxy-agent": "^7.0.0",
+        "https-proxy-agent": "^7.0.6",
+        "pac-resolver": "^7.0.1",
+        "socks-proxy-agent": "^8.0.5"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/pac-resolver": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
+      "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
+      "license": "MIT",
+      "dependencies": {
+        "degenerator": "^5.0.0",
+        "netmask": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/pend": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+      "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
+      "license": "MIT"
+    },
+    "node_modules/progress": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+      "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/proxy-agent": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
+      "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "^4.3.4",
+        "http-proxy-agent": "^7.0.1",
+        "https-proxy-agent": "^7.0.6",
+        "lru-cache": "^7.14.1",
+        "pac-proxy-agent": "^7.1.0",
+        "proxy-from-env": "^1.1.0",
+        "socks-proxy-agent": "^8.0.5"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+      "license": "MIT"
+    },
+    "node_modules/pump": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
+      "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
+      "license": "MIT",
+      "dependencies": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
+    "node_modules/puppeteer-core": {
+      "version": "24.38.0",
+      "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.38.0.tgz",
+      "integrity": "sha512-zB3S/tksIhgi2gZRndUe07AudBz5SXOB7hqG0kEa9/YXWrGwlVlYm3tZtwKgfRftBzbmLQl5iwHkQQl04n/mWw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@puppeteer/browsers": "2.13.0",
+        "chromium-bidi": "14.0.0",
+        "debug": "^4.4.3",
+        "devtools-protocol": "0.0.1581282",
+        "typed-query-selector": "^2.12.1",
+        "webdriver-bidi-protocol": "0.4.1",
+        "ws": "^8.19.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/semver": {
+      "version": "7.7.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+      "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/smart-buffer": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+      "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6.0.0",
+        "npm": ">= 3.0.0"
+      }
+    },
+    "node_modules/socks": {
+      "version": "2.8.7",
+      "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
+      "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
+      "license": "MIT",
+      "dependencies": {
+        "ip-address": "^10.0.1",
+        "smart-buffer": "^4.2.0"
+      },
+      "engines": {
+        "node": ">= 10.0.0",
+        "npm": ">= 3.0.0"
+      }
+    },
+    "node_modules/socks-proxy-agent": {
+      "version": "8.0.5",
+      "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
+      "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "^4.3.4",
+        "socks": "^2.8.3"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "license": "BSD-3-Clause",
+      "optional": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/streamx": {
+      "version": "2.23.0",
+      "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
+      "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
+      "license": "MIT",
+      "dependencies": {
+        "events-universal": "^1.0.0",
+        "fast-fifo": "^1.3.2",
+        "text-decoder": "^1.1.0"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/tar-fs": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz",
+      "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==",
+      "license": "MIT",
+      "dependencies": {
+        "pump": "^3.0.0",
+        "tar-stream": "^3.1.5"
+      },
+      "optionalDependencies": {
+        "bare-fs": "^4.0.1",
+        "bare-path": "^3.0.0"
+      }
+    },
+    "node_modules/tar-stream": {
+      "version": "3.1.8",
+      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz",
+      "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==",
+      "license": "MIT",
+      "dependencies": {
+        "b4a": "^1.6.4",
+        "bare-fs": "^4.5.5",
+        "fast-fifo": "^1.2.0",
+        "streamx": "^2.15.0"
+      }
+    },
+    "node_modules/teex": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
+      "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
+      "license": "MIT",
+      "dependencies": {
+        "streamx": "^2.12.5"
+      }
+    },
+    "node_modules/text-decoder": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
+      "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "b4a": "^1.6.4"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "license": "0BSD"
+    },
+    "node_modules/typed-query-selector": {
+      "version": "2.12.1",
+      "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz",
+      "integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==",
+      "license": "MIT"
+    },
+    "node_modules/undici-types": {
+      "version": "7.18.2",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+      "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/webdriver-bidi-protocol": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz",
+      "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "license": "ISC"
+    },
+    "node_modules/ws": {
+      "version": "8.19.0",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
+      "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/y18n": {
+      "version": "5.0.8",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+      "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yargs": {
+      "version": "17.7.2",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+      "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+      "license": "MIT",
+      "dependencies": {
+        "cliui": "^8.0.1",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.3",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^21.1.1"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yargs-parser": {
+      "version": "21.1.1",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+      "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yauzl": {
+      "version": "2.10.0",
+      "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+      "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
+      "license": "MIT",
+      "dependencies": {
+        "buffer-crc32": "~0.2.3",
+        "fd-slicer": "~1.1.0"
+      }
+    },
+    "node_modules/zod": {
+      "version": "3.25.76",
+      "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+      "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/colinhacks"
+      }
+    }
+  }
+}

+ 5 - 0
prototype-ui/package.json

@@ -0,0 +1,5 @@
+{
+  "dependencies": {
+    "puppeteer-core": "^24.38.0"
+  }
+}

+ 49 - 0
prototype-ui/screenshot.js

@@ -0,0 +1,49 @@
+const puppeteer = require('puppeteer-core');
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+
+function startServer(root, port) {
+  return new Promise(resolve => {
+    const srv = http.createServer((req, res) => {
+      let fp = path.join(root, decodeURIComponent(req.url.split('?')[0]));
+      if (!fs.existsSync(fp) || fs.statSync(fp).isDirectory()) fp = path.join(root, 'index-v2.html');
+      const ext = path.extname(fp).toLowerCase();
+      const mime = { '.html':'text/html', '.js':'application/javascript', '.css':'text/css', '.png':'image/png', '.jpg':'image/jpeg' };
+      try { res.writeHead(200, { 'Content-Type': mime[ext] || 'application/octet-stream' }); fs.createReadStream(fp).pipe(res); }
+      catch(e) { res.writeHead(404); res.end(); }
+    });
+    srv.listen(port, () => resolve(srv));
+  });
+}
+
+(async () => {
+  const root = 'C:\\Users\\Administrator\\lab-safety-monitor';
+  const port = 19527;
+  const srv = await startServer(root, port);
+
+  const browser = await puppeteer.launch({
+    executablePath: 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
+    headless: 'new',
+    args: ['--no-sandbox','--disable-setuid-sandbox','--window-size=9600,2800']
+  });
+
+  const page = await browser.newPage();
+  await page.setViewport({ width: 9600, height: 2800, deviceScaleFactor: 1 });
+  await page.goto(`http://localhost:${port}/index-v2.html`, { waitUntil: 'networkidle0', timeout: 30000 });
+
+  await new Promise(r => setTimeout(r, 2000));
+  // 关闭预警弹窗
+  await page.evaluate(() => {
+    const modal = document.getElementById('alert-modal');
+    if (modal) modal.style.display = 'none';
+    window.showAlert = () => {};
+  });
+  // 等待摄像头初始化
+  await new Promise(r => setTimeout(r, 5000));
+
+  await page.screenshot({ path: 'C:\\Users\\Administrator\\lab-safety-monitor\\design-preview.png', fullPage: true });
+  console.log('Done');
+  await browser.close();
+  srv.close();
+})().catch(e => { console.error(e.message); process.exit(1); });

+ 0 - 16
public/index.html

@@ -1,16 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-  <head>
-    <meta charset="utf-8">
-    <meta http-equiv="X-UA-Compatible" content="IE=edge">
-    <meta name="viewport" content="width=device-width,initial-scale=1.0">
-    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
-    <title>实验室安全智能监测与管控中心</title>
-  </head>
-  <body>
-    <noscript>
-      <strong>请启用JavaScript以获得最佳体验。</strong>
-    </noscript>
-    <div id="app"></div>
-  </body>
-</html>

+ 0 - 18
src/App.vue

@@ -1,18 +0,0 @@
-<template>
-  <div id="app">
-    <router-view />
-  </div>
-</template>
-
-<script>
-export default {
-  name: 'App'
-}
-</script>
-
-<style lang="scss">
-#app {
-  width: 100%;
-  height: 100%;
-}
-</style>

+ 0 - 27
src/api/auth.js

@@ -1,27 +0,0 @@
-/**
- * 模拟登录 API
- * 使用 setTimeout 模拟异步请求
- */
-export function loginApi(data) {
-  return new Promise((resolve) => {
-    setTimeout(() => {
-      // 模拟登录成功,返回 token
-      if (data.username && data.password) {
-        resolve({
-          code: 200,
-          message: '登录成功',
-          data: {
-            token: 'mock_token_' + Date.now(),
-            username: data.username
-          }
-        })
-      } else {
-        resolve({
-          code: 400,
-          message: '用户名或密码不能为空',
-          data: null
-        })
-      }
-    }, 500)
-  })
-}

+ 0 - 220
src/api/screen.js

@@ -1,220 +0,0 @@
-/**
- * 大屏页面模拟数据 - 匹配 index-v2.html
- */
-
-// 实验室基本情况统计
-export function getLabStats() {
-  return new Promise(resolve => {
-    setTimeout(() => {
-      resolve({
-        code: 200,
-        data: {
-          total: 128,
-          levels: [
-            { name: 'I级', label: 'I 级(危险)', value: 12, color: '#cc0000' },
-            { name: 'II级', label: 'II 级(较危险)', value: 28, color: '#ff8000' },
-            { name: 'III级', label: 'III 级(一般)', value: 45, color: '#ffcc00' },
-            { name: 'IV级', label: 'IV 级(较安全)', value: 43, color: '#0066cc' }
-          ],
-          status: { active: 20, warning: 3, idle: 105 }
-        }
-      })
-    }, 300)
-  })
-}
-
-// 实验室安全分级统计
-export function getSafetyLevelStats() {
-  return new Promise(resolve => {
-    setTimeout(() => {
-      resolve({
-        code: 200,
-        data: {
-          levels: ['I级', 'II级', 'III级', 'IV级'],
-          colors: ['#cc0000', '#ff8000', '#ffcc00', '#0066cc'],
-          units: ['化学所', '生物所', '材料所', '物理所', '工程所', '核能所', '信息所', '环境所', '计量所', '医学所'],
-          totals: [28, 22, 18, 24, 16, 12, 20, 15, 10, 14],
-          series: [
-            [2, 3, 5, 4, 2, 1, 3, 2, 1, 2],
-            [5, 6, 4, 7, 4, 3, 5, 4, 3, 4],
-            [9, 8, 6, 8, 5, 5, 7, 5, 4, 5],
-            [12, 5, 3, 5, 5, 3, 5, 4, 2, 3]
-          ]
-        }
-      })
-    }, 300)
-  })
-}
-
-// 实验室进入人数统计及走势
-export function getPersonnelStats() {
-  return new Promise(resolve => {
-    setTimeout(() => {
-      resolve({
-        code: 200,
-        data: {
-          totalEntry: 1284,
-          currentPresent: 47,
-          trend: {
-            times: ['00:00', '03:00', '06:00', '09:00', '12:00', '15:00', '18:00', '21:00', '24:00'],
-            entry: [0, 0, 2, 86, 128, 145, 160, 90, 30],
-            present: [0, 0, 2, 62, 98, 108, 120, 72, 20]
-          }
-        }
-      })
-    }, 300)
-  })
-}
-
-// 智能环境感知应用设备统计
-export function getDeviceStats() {
-  return new Promise(resolve => {
-    setTimeout(() => {
-      resolve({
-        code: 200,
-        data: {
-          online: 312,
-          offline: 18,
-          onlineRate: 94.5,
-          devices: [
-            { name: '电子信息铭牌', value: 86, icon: '🏷️' },
-            { name: '化学品智能终端', value: 44, icon: '⚗️' },
-            { name: '传感器套件', value: 128, icon: '🌡️' },
-            { name: '智能摄像设备', value: 72, icon: '📷' }
-          ]
-        }
-      })
-    }, 300)
-  })
-}
-
-// 实验室设备分类及使用统计
-export function getEquipmentStats() {
-  return new Promise(resolve => {
-    setTimeout(() => {
-      resolve({
-        code: 200,
-        data: {
-          totalEquipment: 2458,
-          totalHours: 18620,
-          usageRate: 62.4,
-          categories: [
-            { value: 680, name: '检测设备', color: '#1e90ff' },
-            { value: 520, name: '分析仪器', color: '#4361ee' },
-            { value: 380, name: '制备设备', color: '#00e676' },
-            { value: 280, name: '安全设备', color: '#ffd740' },
-            { value: 240, name: '辅助设备', color: '#00e5c8' },
-            { value: 358, name: '其他', color: '#f97316' }
-          ],
-          usageStatus: [
-            { value: 486, name: '使用', color: '#1e90ff' },
-            { value: 1840, name: '空闲', color: '#00e676' },
-            { value: 34, name: '维修', color: '#f59e0b' }
-          ]
-        }
-      })
-    }, 300)
-  })
-}
-
-// 实验环境安全智能感知
-export function getSensorList() {
-  return new Promise(resolve => {
-    setTimeout(() => {
-      resolve({
-        code: 200,
-        data: [
-          { name: '化学分析实验室', room: 'A301', unit: '化学研究所', t: 22.5, h: 58, tvoc: 0.82, co2: 650, o2: 20.9, alert: true },
-          { name: '生物安全实验室', room: 'B201', unit: '生物研究所', t: 20.1, h: 62, tvoc: 0.18, co2: 580, o2: 20.8, alert: false },
-          { name: '材料测试实验室', room: 'C401', unit: '材料研究所', t: 24.0, h: 45, tvoc: 0.09, co2: 520, o2: 20.9, alert: false },
-          { name: '精密仪器实验室', room: 'A205', unit: '物理研究所', t: 21.5, h: 50, tvoc: 0.14, co2: 490, o2: 20.9, alert: false },
-          { name: '有机合成实验室', room: 'B302', unit: '化学研究所', t: 23.8, h: 55, tvoc: 2.10, co2: 725, o2: 20.5, alert: true },
-          { name: '光学检测实验室', room: 'C203', unit: '物理研究所', t: 22.0, h: 52, tvoc: 0.11, co2: 510, o2: 20.9, alert: false },
-          { name: '高压实验室', room: 'D101', unit: '工程研究所', t: 25.5, h: 48, tvoc: 0.30, co2: 600, o2: 20.7, alert: false },
-          { name: '低温实验室', room: 'D205', unit: '物理研究所', t: 18.0, h: 40, tvoc: 0.07, co2: 480, o2: 20.9, alert: false },
-          { name: '核磁共振室', room: 'A302', unit: '化学研究所', t: 20.5, h: 54, tvoc: 0.05, co2: 505, o2: 20.9, alert: false },
-          { name: '质谱分析室', room: 'A303', unit: '化学研究所', t: 21.0, h: 53, tvoc: 0.19, co2: 530, o2: 20.8, alert: false }
-        ]
-      })
-    }, 300)
-  })
-}
-
-// 实验室实时风险预警
-export function getWarningList() {
-  return new Promise(resolve => {
-    setTimeout(() => {
-      resolve({
-        code: 200,
-        data: {
-          total: 42,
-          list: [
-            { lab: '化学分析实验室', room: 'A301', unit: '化学研究所', metric: 'TVOC浓度超标', val: '2.85 mg/m³', time: '2026-03-05 14:32:18' },
-            { lab: '有机合成实验室', room: 'B302', unit: '化学研究所', metric: 'CO₂浓度偏高', val: '725 ppm', time: '2026-03-05 13:58:44' },
-            { lab: '高压实验室', room: 'D101', unit: '工程研究所', metric: '温度异常', val: '38.5 °C', time: '2026-03-05 12:15:07' },
-            { lab: '生物安全实验室', room: 'B201', unit: '生物研究所', metric: '湿度超标', val: '88% RH', time: '2026-03-05 11:40:22' },
-            { lab: '材料测试实验室', room: 'C401', unit: '材料研究所', metric: 'TVOC超标', val: '1.2 mg/m³', time: '2026-03-05 10:08:55' },
-            { lab: '光学检测实验室', room: 'C203', unit: '物理研究所', metric: 'O₂浓度偏低', val: '19.2 %', time: '2026-03-05 09:22:33' },
-            { lab: '精密仪器实验室', room: 'A205', unit: '物理研究所', metric: '温度超标', val: '28.5 °C', time: '2026-03-05 08:45:11' },
-            { lab: '低温实验室', room: 'D205', unit: '物理研究所', metric: '气压异常', val: '85 kPa', time: '2026-03-04 23:12:40' }
-          ]
-        }
-      })
-    }, 300)
-  })
-}
-
-// 监控摄像头列表
-export function getCameraList() {
-  return new Promise(resolve => {
-    setTimeout(() => {
-      resolve({
-        code: 200,
-        data: [
-          'A301 有机合成室', 'A302 核磁共振室', 'A303 质谱室',
-          'A301 走廊', 'A302 走廊', 'A303 走廊',
-          'A层公共区域', 'A层安全通道', 'A层出入口'
-        ]
-      })
-    }, 300)
-  })
-}
-
-// 监控树形数据
-export function getMonitorTree() {
-  return new Promise(resolve => {
-    setTimeout(() => {
-      resolve({
-        code: 200,
-        data: {
-          label: '安科院主园区',
-          children: [
-            {
-              label: '综合实验楼A',
-              children: [
-                { label: '1层', children: [{ label: 'A101 化学实验室' }, { label: 'A102 分析室' }] },
-                { label: '2层', children: [{ label: 'A201 生物实验室' }] },
-                { label: '3层', children: [{ label: 'A301 有机合成室' }, { label: 'A302 核磁共振室' }, { label: 'A303 质谱室' }] },
-                { label: '4层', children: [{ label: 'A401 X射线室' }] }
-              ]
-            },
-            {
-              label: '物理实验楼B',
-              children: [
-                { label: '1层', children: [{ label: 'B101 光学实验室' }] },
-                { label: '2层', children: [{ label: 'B201 低温实验室' }] }
-              ]
-            },
-            {
-              label: '工程技术楼C',
-              children: [
-                { label: '1层', children: [{ label: 'C101 机械加工室' }] },
-                { label: '2层', children: [{ label: 'C201 材料测试室' }] }
-              ]
-            }
-          ]
-        }
-      })
-    }, 300)
-  })
-}

+ 0 - 243
src/components/AlarmInfo.vue

@@ -1,243 +0,0 @@
-<template>
-  <div class="panel alarm-info">
-    <!-- Animated border beam -->
-    <div class="border-beam"></div>
-    <!-- Panel header -->
-    <div class="panel-header">
-      <div class="panel-header-icon">&#x26A0;&#xFE0F;</div>
-      <span class="panel-title">实验室实时风险预警</span>
-      <div class="header-count" style="margin-left:auto">
-        <span class="count-label">本月</span>
-        <span class="count-value">{{ totalCount }}</span>
-        <span class="count-label">次</span>
-      </div>
-    </div>
-    <!-- Scrolling warning list -->
-    <div class="warn-scroll-wrap">
-      <div class="warn-scroll-inner">
-        <div
-          v-for="(item, idx) in scrollList"
-          :key="idx"
-          class="warn-item"
-        >
-          <div class="warn-item-head">
-            <span class="warn-lab">&#x1F6A8; {{ item.lab }}({{ item.room }})- {{ item.unit }}</span>
-            <span class="warn-time">{{ item.time }}</span>
-          </div>
-          <div class="warn-detail">
-            异常指标:<span class="warn-metric-val">{{ item.metric }} {{ item.val }}</span>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-import { getWarningList } from '@/api/screen'
-
-export default {
-  name: 'AlarmInfo',
-  data() {
-    return {
-      totalCount: 0,
-      warningList: []
-    }
-  },
-  computed: {
-    /** Duplicate data for seamless CSS scroll loop */
-    scrollList() {
-      return [...this.warningList, ...this.warningList]
-    }
-  },
-  mounted() {
-    this.fetchData()
-  },
-  methods: {
-    async fetchData() {
-      const res = await getWarningList()
-      if (res.code === 200) {
-        this.totalCount = res.data.total
-        this.warningList = res.data.list
-      }
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.alarm-info {
-  position: relative;
-  border-radius: 15px;
-  overflow: hidden;
-  background: $bg-panel;
-  border: 1px solid $border;
-  display: flex;
-  flex-direction: column;
-  height: 100%;
-
-  &::before {
-    content: '';
-    position: absolute;
-    inset: 0;
-    pointer-events: none;
-    border-radius: inherit;
-    background: linear-gradient(135deg, rgba(30,144,255,0.05) 0%, transparent 50%, rgba(0,216,255,0.03) 100%);
-  }
-}
-
-/* ===== Animated border beam ===== */
-.border-beam {
-  position: absolute;
-  inset: 0;
-  pointer-events: none;
-  border-radius: inherit;
-  overflow: hidden;
-  z-index: 2;
-
-  &::before {
-    content: '';
-    position: absolute;
-    top: 0;
-    left: -100%;
-    width: 40%;
-    height: 3px;
-    background: linear-gradient(90deg, transparent, rgba(30,144,255,0.9), rgba(0,216,255,0.7), transparent);
-    animation: beamTop 5s linear infinite;
-  }
-
-  &::after {
-    content: '';
-    position: absolute;
-    bottom: 0;
-    right: -100%;
-    width: 40%;
-    height: 3px;
-    background: linear-gradient(90deg, transparent, rgba(0,216,255,0.7), rgba(30,144,255,0.9), transparent);
-    animation: beamBottom 5s linear infinite 2.5s;
-  }
-}
-
-@keyframes beamTop    { from { left: -40%; }  to { left: 100%; } }
-@keyframes beamBottom { from { right: -40%; } to { right: 100%; } }
-
-/* ===== Panel header ===== */
-.panel-header {
-  display: flex;
-  align-items: center;
-  gap: 25px;
-  padding: 20px 30px 18px;
-  border-bottom: 1px solid $border;
-  background: linear-gradient(90deg, rgba(0,60,160,0.18), transparent);
-  flex-shrink: 0;
-}
-
-.panel-header-icon {
-  width: 65px;
-  height: 65px;
-  border-radius: 12px;
-  flex-shrink: 0;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 32px;
-  background: linear-gradient(135deg, rgba(30,144,255,0.25), rgba(0,216,255,0.15));
-  border: 1px solid rgba(30,144,255,0.35);
-  animation: iconGlow 3s ease-in-out infinite;
-}
-
-@keyframes iconGlow {
-  0%, 100% { box-shadow: 0 0 15px rgba(30,144,255,0.3); }
-  50%      { box-shadow: 0 0 35px rgba(30,144,255,0.7), 0 0 12px rgba(0,216,255,0.3); }
-}
-
-.panel-title {
-  font-size: 30px;
-  font-weight: 600;
-  letter-spacing: 2px;
-  color: $cyan;
-}
-
-/* ===== Header count ===== */
-.header-count {
-  display: flex;
-  align-items: center;
-  gap: 10px;
-  flex-shrink: 0;
-}
-
-.count-label {
-  font-size: 25px;
-  color: $text-dim;
-}
-
-.count-value {
-  font-size: 50px;
-  font-weight: 700;
-  color: #f59e0b;
-}
-
-/* ===== Warning scroll ===== */
-.warn-scroll-wrap {
-  overflow: hidden;
-  flex: 1;
-  min-height: 0;
-  padding: 15px 20px;
-}
-
-.warn-scroll-inner {
-  animation: scrollUp 22s linear infinite;
-
-  &:hover {
-    animation-play-state: paused;
-  }
-}
-
-@keyframes scrollUp {
-  0%   { transform: translateY(0); }
-  100% { transform: translateY(-50%); }
-}
-
-/* ===== Warning item ===== */
-.warn-item {
-  padding: 18px 20px;
-  border-radius: 10px;
-  margin-bottom: 12px;
-  background: rgba(245,158,11,0.05);
-  border: 1px solid rgba(245,158,11,0.2);
-}
-
-.warn-item-head {
-  display: flex;
-  justify-content: space-between;
-  align-items: flex-start;
-  margin-bottom: 10px;
-  gap: 15px;
-}
-
-.warn-lab {
-  font-size: 28px;
-  font-weight: 600;
-  color: #fcd34d;
-  flex: 1;
-}
-
-.warn-time {
-  font-size: 25px;
-  color: $text-dim;
-  white-space: nowrap;
-}
-
-.warn-detail {
-  font-size: 25px;
-  color: $text-dim;
-  display: flex;
-  align-items: center;
-  gap: 10px;
-}
-
-.warn-metric-val {
-  color: #fb923c;
-  font-weight: 600;
-}
-</style>

+ 0 - 144
src/components/CategoryChart.vue

@@ -1,144 +0,0 @@
-<template>
-  <div class="panel category-chart">
-    <div class="panel__title">实验室危险源分类统计</div>
-    <div class="chart-wrap">
-      <div ref="chart" class="chart"></div>
-    </div>
-    <div class="btn-row">
-      <button class="btn-floorplan">返回平面图</button>
-    </div>
-  </div>
-</template>
-
-<script>
-import * as echarts from 'echarts'
-import { getHazardStats } from '@/api/screen'
-
-export default {
-  name: 'CategoryChart',
-  data() {
-    return {
-      total: 0,
-      chart: null
-    }
-  },
-  mounted() {
-    this.fetchData()
-  },
-  beforeDestroy() {
-    if (this.chart) this.chart.dispose()
-  },
-  methods: {
-    async fetchData() {
-      const res = await getHazardStats()
-      if (res.code === 200) {
-        this.total = res.data.total
-        this.$nextTick(() => this.initChart(res.data.categories))
-      }
-    },
-    /** 初始化环形图 - 展示危险源分类分布 */
-    initChart(categories) {
-      this.chart = echarts.init(this.$refs.chart)
-      const option = {
-        color: categories.map(c => c.color),
-        tooltip: {
-          trigger: 'item',
-          textStyle: { fontSize: 24 },
-          formatter: '{b}: {c} ({d}%)'
-        },
-        legend: {
-          orient: 'vertical',
-          right: 10,
-          top: 'center',
-          textStyle: { color: 'rgba(180,210,255,0.85)', fontSize: 24 },
-          itemWidth: 20,
-          itemHeight: 12,
-          itemGap: 18
-        },
-        series: [{
-          type: 'pie',
-          radius: ['42%', '65%'],
-          center: ['35%', '50%'],
-          label: { show: false },
-          data: categories.map(c => ({ name: c.name, value: c.value })),
-          itemStyle: {
-            borderColor: '#0a1a3a',
-            borderWidth: 3
-          }
-        }],
-        graphic: [{
-          type: 'group',
-          left: '23%',
-          top: 'center',
-          children: [
-            {
-              type: 'text',
-              left: 'center',
-              top: -28,
-              style: {
-                text: String(this.total),
-                fill: '#00e5ff',
-                fontSize: 56,
-                fontWeight: 'bold',
-                textAlign: 'center'
-              }
-            },
-            {
-              type: 'text',
-              left: 'center',
-              top: 28,
-              style: {
-                text: '危险源总数',
-                fill: 'rgba(180,210,255,0.7)',
-                fontSize: 24,
-                textAlign: 'center'
-              }
-            }
-          ]
-        }]
-      }
-      this.chart.setOption(option)
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.category-chart {
-  display: flex;
-  flex-direction: column;
-  height: 100%;
-}
-
-.chart-wrap {
-  flex: 1;
-  min-height: 0;
-
-  .chart {
-    width: 100%;
-    height: 100%;
-  }
-}
-
-.btn-row {
-  display: flex;
-  justify-content: center;
-  padding-top: 16px;
-
-  .btn-floorplan {
-    padding: 12px 48px;
-    font-size: $font-size-small;
-    color: $color-cyan;
-    background: rgba(0, 229, 255, 0.1);
-    border: 1px solid rgba(0, 229, 255, 0.4);
-    border-radius: 6px;
-    cursor: pointer;
-    letter-spacing: 4px;
-    transition: all 0.3s;
-
-    &:hover {
-      background: rgba(0, 229, 255, 0.2);
-    }
-  }
-}
-</style>

+ 0 - 330
src/components/EnvMonitorStats.vue

@@ -1,330 +0,0 @@
-<template>
-  <div class="panel env-monitor-stats">
-    <div class="border-beam"></div>
-    <svg class="pc tl" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
-    <svg class="pc tr" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
-    <svg class="pc bl" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
-    <svg class="pc br" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
-    <div class="panel-header">
-      <div class="panel-header-icon">📡</div>
-      <span class="panel-title">智能环境感知应用设备统计</span>
-    </div>
-    <div class="panel-body">
-      <!-- 在线/离线设备 -->
-      <div class="device-online-row">
-        <div class="device-stat-chip online">
-          <div class="dv">{{ online }}</div>
-          <div class="dl">在线设备</div>
-        </div>
-        <div class="device-stat-chip offline">
-          <div class="dv">{{ offline }}</div>
-          <div class="dl">离线设备</div>
-        </div>
-      </div>
-      <!-- 仪表盘 + 设备列表 -->
-      <div class="device-middle">
-        <div ref="gauge" class="gauge-chart"></div>
-        <div class="device-list-row">
-          <div class="device-list-item" v-for="item in devices" :key="item.name">
-            <div class="dli-icon">{{ item.icon }}</div>
-            <div class="dli-num">{{ item.value }}</div>
-            <div class="dli-name">{{ item.name }}</div>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-import * as echarts from 'echarts'
-import { getDeviceStats } from '@/api/screen'
-
-export default {
-  name: 'EnvMonitorStats',
-  data() {
-    return {
-      online: 312,
-      offline: 18,
-      onlineRate: 94.5,
-      devices: [
-        { icon: '🏷️', value: 86, name: '电子信息铭牌' },
-        { icon: '⚗️', value: 44, name: '化学品智能终端' },
-        { icon: '🌡️', value: 128, name: '传感器套件' },
-        { icon: '📷', value: 72, name: '智能摄像设备' }
-      ],
-      chart: null
-    }
-  },
-  mounted() {
-    this.fetchData()
-  },
-  beforeDestroy() {
-    if (this.chart) this.chart.dispose()
-    window.removeEventListener('resize', this.handleResize)
-  },
-  methods: {
-    async fetchData() {
-      try {
-        const res = await getDeviceStats()
-        if (res.code === 200) {
-          this.online = res.data.online
-          this.offline = res.data.offline
-          this.onlineRate = res.data.onlineRate
-          this.devices = res.data.devices
-        }
-      } catch (e) {
-        // use defaults
-      }
-      this.$nextTick(() => this.initChart())
-    },
-    handleResize() {
-      if (this.chart) this.chart.resize()
-    },
-    /** 初始化仪表盘 - 展示设备在线率 */
-    initChart() {
-      this.chart = echarts.init(this.$refs.gauge, null, { renderer: 'canvas', devicePixelRatio: 2 })
-      const option = {
-        backgroundColor: 'transparent',
-        series: [{
-          type: 'gauge',
-          radius: '90%',
-          center: ['50%', '60%'],
-          startAngle: 210,
-          endAngle: -30,
-          min: 0,
-          max: 100,
-          splitNumber: 5,
-          axisLine: {
-            lineStyle: {
-              width: 30,
-              color: [
-                [0.3, '#ef4444'],
-                [0.6, '#f59e0b'],
-                [1, '#1e90ff']
-              ]
-            }
-          },
-          axisTick: { show: false },
-          splitLine: { show: false },
-          axisLabel: { show: false },
-          pointer: {
-            icon: 'path://M12.8,0.7l12.3,0 M0,0 l9.2,12.4 M0,0 l2.2,-1.6',
-            offsetCenter: [0, '-60%'],
-            width: 8,
-            length: '60%',
-            itemStyle: { color: '#1e90ff' }
-          },
-          detail: {
-            valueAnimation: true,
-            formatter: '{value}%',
-            color: '#ffd740',
-            fontSize: 45,
-            fontWeight: 700,
-            offsetCenter: [0, '30%']
-          },
-          title: {
-            show: true,
-            offsetCenter: [0, '62%'],
-            color: 'rgba(110,165,210,0.6)',
-            fontSize: 25
-          },
-          data: [{ value: this.onlineRate, name: '在线率' }]
-        }]
-      }
-      this.chart.setOption(option)
-      window.addEventListener('resize', this.handleResize)
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.env-monitor-stats {
-  position: relative;
-  display: flex;
-  flex-direction: column;
-}
-
-/* ---- Border beam ---- */
-.border-beam {
-  position: absolute;
-  inset: 0;
-  pointer-events: none;
-  border-radius: inherit;
-  overflow: hidden;
-  z-index: 2;
-
-  &::before {
-    content: '';
-    position: absolute;
-    top: 0;
-    left: -100%;
-    width: 40%;
-    height: 3px;
-    background: linear-gradient(90deg, transparent, rgba(30, 144, 255, 0.9), rgba(0, 216, 255, 0.7), transparent);
-    animation: beamTop 5s linear infinite;
-  }
-
-  &::after {
-    content: '';
-    position: absolute;
-    bottom: 0;
-    right: -100%;
-    width: 40%;
-    height: 3px;
-    background: linear-gradient(90deg, transparent, rgba(0, 216, 255, 0.7), rgba(30, 144, 255, 0.9), transparent);
-    animation: beamBottom 5s linear infinite 2.5s;
-  }
-}
-
-@keyframes beamTop    { from { left: -40%; } to { left: 100%; } }
-@keyframes beamBottom { from { right: -40%; } to { right: 100%; } }
-
-/* ---- Corner ornaments ---- */
-.pc {
-  position: absolute;
-  width: 35px;
-  height: 35px;
-  z-index: 3;
-  pointer-events: none;
-
-  &.tl { top: 0; left: 0; }
-  &.tr { top: 0; right: 0; transform: scaleX(-1); }
-  &.bl { bottom: 0; left: 0; transform: scaleY(-1); }
-  &.br { bottom: 0; right: 0; transform: scale(-1); }
-
-  svg { width: 100%; height: 100%; }
-}
-
-/* ---- Panel header ---- */
-.panel-header {
-  display: flex;
-  align-items: center;
-  gap: 25px;
-  padding: 20px 30px 18px;
-  border-bottom: 1px solid $border;
-  background: linear-gradient(90deg, rgba(0, 60, 160, 0.18), transparent);
-  flex-shrink: 0;
-}
-
-.panel-header-icon {
-  width: 65px;
-  height: 65px;
-  border-radius: 12px;
-  flex-shrink: 0;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 32px;
-  background: linear-gradient(135deg, rgba(30, 144, 255, 0.25), rgba(0, 216, 255, 0.15));
-  border: 1px solid rgba(30, 144, 255, 0.35);
-  animation: iconGlow 3s ease-in-out infinite;
-}
-
-@keyframes iconGlow {
-  0%, 100% { box-shadow: 0 0 15px rgba(30, 144, 255, 0.3); }
-  50% { box-shadow: 0 0 35px rgba(30, 144, 255, 0.7), 0 0 12px rgba(0, 216, 255, 0.3); }
-}
-
-.panel-title {
-  font-size: 30px;
-  font-weight: 600;
-  letter-spacing: 2px;
-  color: $cyan;
-}
-
-/* ---- Panel body ---- */
-.panel-body {
-  padding: 20px 25px;
-}
-
-/* ---- Online / Offline row ---- */
-.device-online-row {
-  display: flex;
-  gap: 18px;
-  margin-bottom: 18px;
-}
-
-.device-stat-chip {
-  flex: 1;
-  padding: 15px;
-  border-radius: 10px;
-  text-align: center;
-  background: $bg-card;
-  border: 1px solid $border;
-
-  &.online {
-    border-color: $green;
-  }
-
-  &.offline {
-    border-color: $red;
-  }
-
-  .dv {
-    font-size: 50px;
-    font-weight: 700;
-  }
-
-  &.online .dv {
-    color: $green;
-  }
-
-  &.offline .dv {
-    color: $red;
-  }
-
-  .dl {
-    font-size: 25px;
-    color: $text-dim;
-  }
-}
-
-/* ---- Middle: gauge + device grid ---- */
-.device-middle {
-  display: flex;
-  gap: 20px;
-  align-items: center;
-}
-
-.gauge-chart {
-  flex: 0 0 295px;
-  height: 295px;
-}
-
-.device-list-row {
-  flex: 1;
-  display: grid;
-  grid-template-columns: 1fr 1fr;
-  gap: 12px;
-}
-
-.device-list-item {
-  padding: 15px;
-  border-radius: 10px;
-  background: $bg-card;
-  border: 1px solid $border;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  gap: 8px;
-
-  .dli-icon {
-    font-size: 40px;
-  }
-
-  .dli-num {
-    font-size: 38px;
-    font-weight: 700;
-    color: $gold;
-  }
-
-  .dli-name {
-    font-size: 22px;
-    color: $text-dim;
-    text-align: center;
-    line-height: 1.3;
-  }
-}
-</style>

+ 0 - 403
src/components/EquipmentStats.vue

@@ -1,403 +0,0 @@
-<template>
-  <div class="panel equipment-stats">
-    <div class="border-beam"></div>
-    <div class="panel-header">
-      <div class="panel-header-icon">🔬</div>
-      <span class="panel-title">实验室设备分类及使用统计</span>
-    </div>
-    <!-- 4:2:4 vertical layout -->
-    <div class="equip-layout">
-      <!-- Top: ring/donut chart - equipment classification -->
-      <div class="equip-section equip-top">
-        <div ref="ringChart" class="ring-chart"></div>
-      </div>
-      <!-- Middle: 3 stat items -->
-      <div class="equip-section equip-mid">
-        <div class="equip-mid-row">
-          <div class="equip-stat-item">
-            <div class="ev">{{ totalEquipment.toLocaleString() }}</div>
-            <div class="el">设备总数(台)</div>
-          </div>
-          <div class="equip-stat-item">
-            <div class="ev">{{ totalHours.toLocaleString() }}</div>
-            <div class="el">使用时长(h)</div>
-          </div>
-          <div class="equip-stat-item">
-            <div class="ev">{{ usageRate }}%</div>
-            <div class="el">设备使用率</div>
-          </div>
-        </div>
-      </div>
-      <!-- Bottom: pie chart - usage status -->
-      <div class="equip-section equip-bottom">
-        <div ref="pieChart" class="pie-chart"></div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-import * as echarts from 'echarts'
-import { getEquipmentStats } from '@/api/screen'
-
-const TOOLTIP_CFG = {
-  backgroundColor: 'rgba(3,14,42,0.92)',
-  borderColor: 'rgba(30,144,255,0.3)',
-  textStyle: { color: '#a8cce8', fontSize: 28 }
-}
-
-export default {
-  name: 'EquipmentStats',
-  data() {
-    return {
-      totalEquipment: 2458,
-      totalHours: 18620,
-      usageRate: 62.4,
-      categories: [
-        { value: 680, name: '检测设备', color: '#1e90ff' },
-        { value: 520, name: '分析仪器', color: '#4361ee' },
-        { value: 380, name: '制备设备', color: '#00e676' },
-        { value: 280, name: '安全设备', color: '#ffd740' },
-        { value: 240, name: '辅助设备', color: '#00e5c8' },
-        { value: 358, name: '其他', color: '#f97316' }
-      ],
-      usageStatus: [
-        { value: 486, name: '使用', color: '#1e90ff' },
-        { value: 1840, name: '空闲', color: '#00e676' },
-        { value: 34, name: '维修', color: '#f59e0b' }
-      ],
-      ringChart: null,
-      pieChart: null
-    }
-  },
-  mounted() {
-    this.fetchData()
-  },
-  beforeDestroy() {
-    if (this.ringChart) this.ringChart.dispose()
-    if (this.pieChart) this.pieChart.dispose()
-    window.removeEventListener('resize', this.handleResize)
-  },
-  methods: {
-    async fetchData() {
-      try {
-        const res = await getEquipmentStats()
-        if (res.code === 200) {
-          this.totalEquipment = res.data.totalEquipment
-          this.totalHours = res.data.totalHours
-          this.usageRate = res.data.usageRate
-          this.categories = res.data.categories
-          this.usageStatus = res.data.usageStatus
-        }
-      } catch (e) {
-        // use defaults
-      }
-      this.$nextTick(() => {
-        this.initRingChart()
-        this.initPieChart()
-        window.addEventListener('resize', this.handleResize)
-      })
-    },
-    handleResize() {
-      if (this.ringChart) this.ringChart.resize()
-      if (this.pieChart) this.pieChart.resize()
-    },
-    /** 初始化环形图 - 设备分类 */
-    initRingChart() {
-      this.ringChart = echarts.init(this.$refs.ringChart, null, { renderer: 'canvas', devicePixelRatio: 2 })
-
-      // Build count map for legend formatter
-      const countMap = {}
-      this.categories.forEach(c => { countMap[c.name] = c.value })
-
-      const ringData = this.categories.map(c => ({
-        value: c.value,
-        name: c.name,
-        itemStyle: {
-          color: c.color,
-          shadowBlur: 8,
-          shadowColor: c.color.replace(')', ',0.5)').replace('rgb', 'rgba')
-        }
-      }))
-
-      const option = {
-        backgroundColor: 'transparent',
-        tooltip: {
-          trigger: 'item',
-          formatter: '{b}: {c}台 ({d}%)',
-          ...TOOLTIP_CFG
-        },
-        legend: {
-          orient: 'vertical',
-          right: '1%',
-          top: 'middle',
-          icon: 'circle',
-          itemWidth: 28,
-          itemHeight: 28,
-          itemGap: 22,
-          textStyle: { color: '#a8cce8', fontSize: 22 },
-          formatter: function(name) {
-            return '{nm|' + name + '}  {vl|' + countMap[name] + '台}'
-          },
-          rich: {
-            nm: { fontSize: 22, color: '#a8cce8', width: 90 },
-            vl: { fontSize: 26, fontWeight: 700, color: '#fff' }
-          }
-        },
-        series: [{
-          type: 'pie',
-          radius: ['38%', '62%'],
-          center: ['36%', '50%'],
-          itemStyle: {
-            borderRadius: 4,
-            borderColor: 'rgba(3,14,31,0.5)',
-            borderWidth: 2
-          },
-          label: {
-            show: true,
-            formatter: '{c}台',
-            fontSize: 20,
-            color: '#a8cce8'
-          },
-          labelLine: {
-            length: 10,
-            length2: 10,
-            lineStyle: { color: 'rgba(30,144,255,0.4)', width: 2 }
-          },
-          data: ringData,
-          emphasis: {
-            scale: true,
-            scaleSize: 5,
-            itemStyle: {
-              shadowBlur: 20,
-              shadowColor: 'rgba(30,144,255,0.6)'
-            }
-          }
-        }]
-      }
-      this.ringChart.setOption(option)
-    },
-    /** 初始化饼图 - 设备使用状态 */
-    initPieChart() {
-      this.pieChart = echarts.init(this.$refs.pieChart, null, { renderer: 'canvas', devicePixelRatio: 2 })
-
-      // Build count map for legend formatter
-      const countMap = {}
-      this.usageStatus.forEach(s => { countMap[s.name] = s.value })
-
-      const pieData = this.usageStatus.map(s => ({
-        value: s.value,
-        name: s.name,
-        itemStyle: {
-          color: s.color,
-          shadowBlur: 10,
-          shadowColor: s.color.replace(')', ',0.5)').replace('rgb', 'rgba')
-        }
-      }))
-
-      const option = {
-        backgroundColor: 'transparent',
-        tooltip: {
-          trigger: 'item',
-          formatter: '{b}: {c}台 ({d}%)',
-          ...TOOLTIP_CFG
-        },
-        legend: {
-          show: true,
-          orient: 'vertical',
-          right: '2%',
-          top: 'middle',
-          icon: 'circle',
-          itemWidth: 36,
-          itemHeight: 36,
-          itemGap: 40,
-          textStyle: { color: '#a8cce8', fontSize: 28 },
-          formatter: function(name) {
-            return '{nm|' + name + '}  {vl|' + countMap[name] + '台}'
-          },
-          rich: {
-            nm: { fontSize: 28, color: '#a8cce8', width: 90 },
-            vl: { fontSize: 34, fontWeight: 700, color: '#fff' }
-          }
-        },
-        series: [{
-          type: 'pie',
-          radius: ['36%', '62%'],
-          center: ['38%', '52%'],
-          itemStyle: {
-            borderRadius: 5,
-            borderColor: 'rgba(3,14,31,0.5)',
-            borderWidth: 2
-          },
-          label: {
-            show: true,
-            formatter: '{b}\n{c}台',
-            fontSize: 24,
-            color: '#a8cce8',
-            lineHeight: 36,
-            distanceToLabelLine: 6
-          },
-          labelLine: {
-            length: 18,
-            length2: 14,
-            lineStyle: { color: 'rgba(30,144,255,0.4)', width: 2 }
-          },
-          data: pieData,
-          emphasis: {
-            scale: true,
-            scaleSize: 6,
-            itemStyle: {
-              shadowBlur: 25,
-              shadowColor: 'rgba(30,144,255,0.6)'
-            }
-          }
-        }]
-      }
-      this.pieChart.setOption(option)
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.equipment-stats {
-  position: relative;
-  display: flex;
-  flex-direction: column;
-  height: 100%;
-}
-
-/* ---- Border beam ---- */
-.border-beam {
-  position: absolute;
-  inset: 0;
-  pointer-events: none;
-  border-radius: inherit;
-  overflow: hidden;
-  z-index: 2;
-
-  &::before {
-    content: '';
-    position: absolute;
-    top: 0;
-    left: -100%;
-    width: 40%;
-    height: 3px;
-    background: linear-gradient(90deg, transparent, rgba(30, 144, 255, 0.9), rgba(0, 216, 255, 0.7), transparent);
-    animation: beamTop 5s linear infinite;
-  }
-
-  &::after {
-    content: '';
-    position: absolute;
-    bottom: 0;
-    right: -100%;
-    width: 40%;
-    height: 3px;
-    background: linear-gradient(90deg, transparent, rgba(0, 216, 255, 0.7), rgba(30, 144, 255, 0.9), transparent);
-    animation: beamBottom 5s linear infinite 2.5s;
-  }
-}
-
-@keyframes beamTop    { from { left: -40%; } to { left: 100%; } }
-@keyframes beamBottom { from { right: -40%; } to { right: 100%; } }
-
-/* ---- Panel header ---- */
-.panel-header {
-  display: flex;
-  align-items: center;
-  gap: 25px;
-  padding: 20px 30px 18px;
-  border-bottom: 1px solid $border;
-  background: linear-gradient(90deg, rgba(0, 60, 160, 0.18), transparent);
-  flex-shrink: 0;
-}
-
-.panel-header-icon {
-  width: 65px;
-  height: 65px;
-  border-radius: 12px;
-  flex-shrink: 0;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 32px;
-  background: linear-gradient(135deg, rgba(30, 144, 255, 0.25), rgba(0, 216, 255, 0.15));
-  border: 1px solid rgba(30, 144, 255, 0.35);
-  animation: iconGlow 3s ease-in-out infinite;
-}
-
-@keyframes iconGlow {
-  0%, 100% { box-shadow: 0 0 15px rgba(30, 144, 255, 0.3); }
-  50% { box-shadow: 0 0 35px rgba(30, 144, 255, 0.7), 0 0 12px rgba(0, 216, 255, 0.3); }
-}
-
-.panel-title {
-  font-size: 30px;
-  font-weight: 600;
-  letter-spacing: 2px;
-  color: $cyan;
-}
-
-/* ---- 4:2:4 Layout ---- */
-.equip-layout {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  padding: 15px 25px;
-  gap: 12px;
-  min-height: 0;
-}
-
-.equip-section {
-  min-height: 0;
-}
-
-.equip-top {
-  flex: 4;
-}
-
-.equip-mid {
-  flex: 2;
-  display: flex;
-  align-items: center;
-}
-
-.equip-bottom {
-  flex: 4;
-}
-
-.ring-chart,
-.pie-chart {
-  width: 100%;
-  height: 100%;
-}
-
-/* ---- Middle stat row ---- */
-.equip-mid-row {
-  display: flex;
-  gap: 15px;
-  width: 100%;
-}
-
-.equip-stat-item {
-  flex: 1;
-  text-align: center;
-  padding: 15px 10px;
-  border-radius: 10px;
-  background: $bg-card;
-  border: 1px solid $border;
-
-  .ev {
-    font-size: 38px;
-    font-weight: 700;
-    color: $gold;
-  }
-
-  .el {
-    font-size: 22px;
-    color: $text-dim;
-    margin-top: 5px;
-  }
-}
-</style>

+ 0 - 570
src/components/EventStats.vue

@@ -1,570 +0,0 @@
-<template>
-  <div class="panel lab-stats">
-    <!-- Border beam animation -->
-    <div class="border-beam"></div>
-    <!-- Corner ornaments -->
-    <svg class="pc tl" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
-    <svg class="pc tr" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
-    <svg class="pc bl" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
-    <svg class="pc br" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
-    <!-- Panel header -->
-    <div class="panel-header">
-      <div class="panel-header-icon">🏛️</div>
-      <span class="panel-title">实验室基本情况统计</span>
-      <div class="status-dot green"></div>
-    </div>
-
-    <!-- Upper section: left gauge + right donut -->
-    <div class="upper-section">
-      <!-- Left: SVG gauge showing total -->
-      <div class="gauge-side">
-        <svg width="320" height="320" viewBox="0 0 320 320">
-          <defs>
-            <linearGradient id="arcG1" x1="0%" y1="0%" x2="100%" y2="100%">
-              <stop offset="0%" stop-color="#1e90ff"/>
-              <stop offset="100%" stop-color="#00d8ff"/>
-            </linearGradient>
-            <filter id="glow1">
-              <feGaussianBlur stdDeviation="4" result="blur"/>
-              <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
-            </filter>
-          </defs>
-          <!-- Outermost decorative ring -->
-          <circle cx="160" cy="160" r="148" fill="none" stroke="rgba(30,144,255,0.08)" stroke-width="1" stroke-dasharray="6 6"/>
-          <!-- Outer track -->
-          <circle cx="160" cy="160" r="136" fill="none" stroke="rgba(30,144,255,0.14)" stroke-width="3"/>
-          <!-- Main arc 300deg -->
-          <circle cx="160" cy="160" r="136" fill="none" stroke="url(#arcG1)" stroke-width="8"
-            stroke-dasharray="711 143" stroke-linecap="round" transform="rotate(-240 160 160)" filter="url(#glow1)"/>
-          <!-- Inner track -->
-          <circle cx="160" cy="160" r="110" fill="rgba(3,14,31,0.7)" stroke="rgba(0,216,255,0.15)" stroke-width="2"/>
-          <!-- Four-level color arcs -->
-          <circle cx="160" cy="160" r="88" fill="none" stroke="#cc0000" stroke-width="12"
-            :stroke-dasharray="levelArcs[0].dash" stroke-linecap="round" transform="rotate(-90 160 160)" opacity="0.9"/>
-          <circle cx="160" cy="160" r="88" fill="none" stroke="#ff8000" stroke-width="12"
-            :stroke-dasharray="levelArcs[1].dash" stroke-linecap="round" :transform="'rotate(' + levelArcs[1].rotate + ' 160 160)'" opacity="0.9"/>
-          <circle cx="160" cy="160" r="88" fill="none" stroke="#ffcc00" stroke-width="12"
-            :stroke-dasharray="levelArcs[2].dash" stroke-linecap="round" :transform="'rotate(' + levelArcs[2].rotate + ' 160 160)'" opacity="0.9"/>
-          <circle cx="160" cy="160" r="88" fill="none" stroke="#0066cc" stroke-width="12"
-            :stroke-dasharray="levelArcs[3].dash" stroke-linecap="round" :transform="'rotate(' + levelArcs[3].rotate + ' 160 160)'" opacity="0.9"/>
-          <!-- Endpoint glow -->
-          <circle cx="160" cy="24" r="6" fill="#1e90ff" opacity="0.9"/>
-          <!-- Center number -->
-          <text x="160" y="145" text-anchor="middle" fill="#ffd740" font-size="72" font-weight="900" font-family="Arial,sans-serif" letter-spacing="-2">{{ total }}</text>
-          <text x="160" y="190" text-anchor="middle" fill="rgba(168,204,232,0.75)" font-size="26" font-family="Arial,sans-serif" letter-spacing="4">间</text>
-        </svg>
-        <!-- Labels -->
-        <div class="gauge-label">
-          <div class="gauge-label-main">实验室总数</div>
-          <div class="gauge-label-sub">TOTAL LABORATORIES</div>
-        </div>
-        <!-- Color bar showing proportions -->
-        <div class="color-bar-wrap">
-          <div class="color-bar">
-            <div v-for="level in levels" :key="level.name" :style="{ flex: level.value, background: level.color, boxShadow: '0 0 10px ' + level.color + '80' }"></div>
-          </div>
-          <div class="color-bar-labels">
-            <span v-for="level in levels" :key="'lbl-' + level.name" :style="{ flex: level.value }">{{ level.name.replace('级', '') }}</span>
-          </div>
-        </div>
-      </div>
-
-      <!-- Right: ECharts donut + level detail list -->
-      <div class="donut-side">
-        <div ref="donutChart" class="donut-chart"></div>
-        <div class="level-list">
-          <div
-            v-for="level in levels"
-            :key="'detail-' + level.name"
-            class="level-item"
-            :style="{ background: level.color + '12', borderLeftColor: level.color }"
-          >
-            <span class="level-item-left">
-              <span class="level-dot" :style="{ background: level.color, boxShadow: '0 0 8px ' + level.color + 'cc' }"></span>
-              {{ level.label }}
-            </span>
-            <span class="level-item-val" :style="{ color: levelValColor(level.color) }">{{ level.value }}间</span>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <!-- Lower section: 3 status badges -->
-    <div class="lower-section">
-      <div class="status-row">
-        <div class="status-badge active">
-          <div class="val">{{ status.active }}</div>
-          <div class="lbl">使用(间)</div>
-        </div>
-        <div class="status-badge warning">
-          <div class="val">{{ status.warning }}</div>
-          <div class="lbl">异常(间)</div>
-        </div>
-        <div class="status-badge idle">
-          <div class="val">{{ status.idle }}</div>
-          <div class="lbl">空闲(间)</div>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-import * as echarts from 'echarts'
-import { getLabStats } from '@/api/screen'
-
-export default {
-  name: 'EventStats',
-  data() {
-    return {
-      total: 0,
-      levels: [],
-      status: { active: 0, warning: 0, idle: 0 },
-      chart: null
-    }
-  },
-  computed: {
-    /** 计算SVG仪表盘中四个分级色弧的dasharray和旋转角度 */
-    levelArcs() {
-      if (!this.levels.length) {
-        return [
-          { dash: '0 553', rotate: -90 },
-          { dash: '0 553', rotate: -90 },
-          { dash: '0 553', rotate: -90 },
-          { dash: '0 553', rotate: -90 }
-        ]
-      }
-      const circumference = 2 * Math.PI * 88 // ~553
-      const totalVal = this.levels.reduce((s, l) => s + l.value, 0)
-      const arcs = []
-      let accAngle = -90 // start at top
-      for (let i = 0; i < this.levels.length; i++) {
-        const ratio = this.levels[i].value / totalVal
-        const arcLen = Math.round(ratio * circumference)
-        const gapLen = Math.round(circumference - arcLen)
-        arcs.push({
-          dash: arcLen + ' ' + gapLen,
-          rotate: Math.round(accAngle)
-        })
-        accAngle += ratio * 360
-      }
-      return arcs
-    }
-  },
-  mounted() {
-    this.fetchData()
-  },
-  beforeDestroy() {
-    if (this.chart) {
-      this.chart.dispose()
-      this.chart = null
-    }
-  },
-  methods: {
-    async fetchData() {
-      const res = await getLabStats()
-      if (res.code === 200) {
-        this.total = res.data.total
-        this.levels = res.data.levels
-        this.status = res.data.status
-        this.$nextTick(() => this.initDonutChart())
-      }
-    },
-    /** 根据分级主色计算稍亮的数值展示色 */
-    levelValColor(color) {
-      const map = {
-        '#cc0000': '#ff6666',
-        '#ff8000': '#ffaa44',
-        '#ffcc00': '#ffe066',
-        '#0066cc': '#4499ff'
-      }
-      return map[color] || color
-    },
-    /** 初始化ECharts环形图 - 展示四级分类占比,中心显示总数 */
-    initDonutChart() {
-      this.chart = echarts.init(this.$refs.donutChart, null, {
-        renderer: 'canvas',
-        devicePixelRatio: 2
-      })
-      const option = {
-        backgroundColor: 'transparent',
-        tooltip: {
-          trigger: 'item',
-          formatter: '{b}: {c}间 ({d}%)',
-          backgroundColor: 'rgba(3,14,42,0.92)',
-          borderColor: 'rgba(30,144,255,0.3)',
-          textStyle: { color: '#a8cce8', fontSize: 28 }
-        },
-        graphic: [
-          {
-            type: 'text',
-            left: 'center',
-            top: '40%',
-            style: {
-              text: String(this.total),
-              fill: '#ffd740',
-              font: 'bold 80px Arial',
-              textAlign: 'center'
-            }
-          },
-          {
-            type: 'text',
-            left: 'center',
-            top: '58%',
-            style: {
-              text: '实验室总数(间)',
-              fill: 'rgba(168,204,232,0.65)',
-              font: '22px Arial',
-              textAlign: 'center'
-            }
-          }
-        ],
-        series: [{
-          type: 'pie',
-          radius: ['42%', '68%'],
-          center: ['50%', '50%'],
-          startAngle: 100,
-          itemStyle: {
-            borderRadius: 6,
-            borderColor: 'rgba(3,14,31,0.6)',
-            borderWidth: 3
-          },
-          label: {
-            show: true,
-            formatter: function (params) {
-              return '{lvl|' + params.name + '}\n{cnt|' + params.value + '间}\n{pct|' + params.percent + '%}'
-            },
-            rich: {
-              lvl: { fontSize: 24, fontWeight: 700, color: '#ddf0ff', lineHeight: 36 },
-              cnt: { fontSize: 30, fontWeight: 900, color: '#ffd740', lineHeight: 42 },
-              pct: { fontSize: 22, color: 'rgba(168,204,232,0.6)', lineHeight: 32 }
-            },
-            distanceToLabelLine: 8
-          },
-          labelLine: {
-            show: true,
-            length: 20,
-            length2: 25,
-            lineStyle: { color: 'rgba(30,144,255,0.45)', width: 2 }
-          },
-          data: this.levels.map(function (l) {
-            return {
-              value: l.value,
-              name: l.name,
-              itemStyle: {
-                color: l.color,
-                shadowBlur: 12,
-                shadowColor: l.color + '80'
-              }
-            }
-          }),
-          emphasis: {
-            scale: true,
-            scaleSize: 8,
-            itemStyle: { shadowBlur: 30, shadowColor: 'rgba(30,144,255,0.6)' },
-            label: { fontSize: 28 }
-          }
-        }]
-      }
-      this.chart.setOption(option)
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.lab-stats {
-  display: flex;
-  flex-direction: column;
-  position: relative;
-  border-radius: 15px;
-  overflow: hidden;
-  background: $bg-panel;
-  border: 1px solid $border;
-}
-
-.lab-stats::before {
-  content: '';
-  position: absolute;
-  inset: 0;
-  pointer-events: none;
-  border-radius: inherit;
-  background: linear-gradient(135deg, rgba(30, 144, 255, 0.05) 0%, transparent 50%, rgba(0, 216, 255, 0.03) 100%);
-}
-
-/* ---- Border beam ---- */
-.border-beam {
-  position: absolute;
-  inset: 0;
-  pointer-events: none;
-  border-radius: inherit;
-  overflow: hidden;
-  z-index: 2;
-
-  &::before {
-    content: '';
-    position: absolute;
-    top: 0;
-    left: -100%;
-    width: 40%;
-    height: 3px;
-    background: linear-gradient(90deg, transparent, rgba(30, 144, 255, 0.9), rgba(0, 216, 255, 0.7), transparent);
-    animation: beamTop 5s linear infinite;
-  }
-
-  &::after {
-    content: '';
-    position: absolute;
-    bottom: 0;
-    right: -100%;
-    width: 40%;
-    height: 3px;
-    background: linear-gradient(90deg, transparent, rgba(0, 216, 255, 0.7), rgba(30, 144, 255, 0.9), transparent);
-    animation: beamBottom 5s linear infinite 2.5s;
-  }
-}
-
-@keyframes beamTop    { from { left: -40%; }  to { left: 100%; } }
-@keyframes beamBottom { from { right: -40%; } to { right: 100%; } }
-
-/* ---- Corner ornaments ---- */
-.pc {
-  position: absolute;
-  width: 35px;
-  height: 35px;
-  z-index: 3;
-  pointer-events: none;
-
-  &.tl { top: 0; left: 0; }
-  &.tr { top: 0; right: 0; transform: scaleX(-1); }
-  &.bl { bottom: 0; left: 0; transform: scaleY(-1); }
-  &.br { bottom: 0; right: 0; transform: scale(-1); }
-}
-
-/* ---- Panel header ---- */
-.panel-header {
-  display: flex;
-  align-items: center;
-  gap: 25px;
-  padding: 20px 30px 18px;
-  border-bottom: 1px solid $border;
-  background: linear-gradient(90deg, rgba(0, 60, 160, 0.18), transparent);
-  flex-shrink: 0;
-}
-
-.panel-header-icon {
-  width: 65px;
-  height: 65px;
-  border-radius: 12px;
-  flex-shrink: 0;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 32px;
-  background: linear-gradient(135deg, rgba(30, 144, 255, 0.25), rgba(0, 216, 255, 0.15));
-  border: 1px solid rgba(30, 144, 255, 0.35);
-  animation: iconGlow 3s ease-in-out infinite;
-}
-
-@keyframes iconGlow {
-  0%, 100% { box-shadow: 0 0 15px rgba(30, 144, 255, 0.3); }
-  50%      { box-shadow: 0 0 35px rgba(30, 144, 255, 0.7), 0 0 12px rgba(0, 216, 255, 0.3); }
-}
-
-.panel-title {
-  font-size: 30px;
-  font-weight: 600;
-  letter-spacing: 2px;
-  color: $cyan;
-}
-
-/* ---- Status dot ---- */
-.status-dot {
-  width: 20px;
-  height: 20px;
-  border-radius: 50%;
-  display: inline-block;
-  margin-left: auto;
-  animation: dotPulse 2s ease-in-out infinite;
-
-  &.green {
-    background: $green;
-    box-shadow: 0 0 15px $green;
-  }
-}
-
-@keyframes dotPulse {
-  0%, 100% { transform: scale(1); opacity: 1; }
-  50%      { transform: scale(1.3); opacity: 0.7; }
-}
-
-/* ---- Upper section ---- */
-.upper-section {
-  flex: 4;
-  display: flex;
-  min-height: 0;
-  overflow: hidden;
-}
-
-/* Left gauge side */
-.gauge-side {
-  flex: 4;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  gap: 18px;
-  padding: 25px 20px;
-  border-right: 1px solid $border;
-  background: linear-gradient(180deg, rgba(30, 144, 255, 0.04), transparent);
-}
-
-.gauge-label {
-  text-align: center;
-
-  &-main {
-    font-size: 30px;
-    font-weight: 700;
-    color: $cyan;
-    letter-spacing: 4px;
-  }
-
-  &-sub {
-    font-size: 22px;
-    color: $text-dim;
-    margin-top: 6px;
-    letter-spacing: 2px;
-  }
-}
-
-.color-bar-wrap {
-  width: 100%;
-  padding: 0 10px;
-}
-
-.color-bar {
-  display: flex;
-  gap: 5px;
-  height: 12px;
-  border-radius: 6px;
-  overflow: hidden;
-}
-
-.color-bar-labels {
-  display: flex;
-  gap: 5px;
-  margin-top: 8px;
-  font-size: 22px;
-  color: $text-dim;
-  text-align: center;
-}
-
-/* Right donut side */
-.donut-side {
-  flex: 6;
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  padding: 25px 25px 20px;
-  gap: 0;
-}
-
-.donut-chart {
-  flex: 0 0 340px;
-  width: 100%;
-}
-
-/* ---- Level detail list ---- */
-.level-list {
-  display: flex;
-  flex-direction: column;
-  gap: 10px;
-  margin-top: 10px;
-}
-
-.level-item {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  font-size: 26px;
-  padding: 10px 16px;
-  border-radius: 8px;
-  border-left: 4px solid transparent;
-
-  &-left {
-    display: flex;
-    align-items: center;
-    gap: 12px;
-  }
-
-  &-val {
-    font-weight: 700;
-    font-size: 28px;
-  }
-}
-
-.level-dot {
-  width: 16px;
-  height: 16px;
-  border-radius: 3px;
-  display: inline-block;
-}
-
-/* ---- Lower section ---- */
-.lower-section {
-  flex: 1;
-  display: flex;
-  align-items: center;
-  padding: 0 25px 20px;
-  border-top: 1px solid $border;
-  gap: 15px;
-  min-height: 0;
-}
-
-.status-row {
-  display: flex;
-  gap: 15px;
-  width: 100%;
-}
-
-.status-badge {
-  flex: 1;
-  padding: 15px 10px;
-  border-radius: 10px;
-  text-align: center;
-  background: $bg-card;
-  border: 1px solid $border;
-
-  &.active {
-    border-color: $green;
-    background: rgba(0, 230, 118, 0.07);
-
-    .val { color: $green; }
-  }
-
-  &.warning {
-    border-color: #f59e0b;
-    background: rgba(245, 158, 11, 0.07);
-
-    .val { color: #f59e0b; }
-  }
-
-  &.idle {
-    border-color: $indigo;
-    background: rgba(67, 97, 238, 0.07);
-
-    .val { color: $indigo; }
-  }
-
-  .val {
-    font-size: 48px;
-    font-weight: 700;
-  }
-
-  .lbl {
-    font-size: 25px;
-    color: $text-dim;
-    margin-top: 5px;
-  }
-}
-</style>

+ 0 - 310
src/components/LabEnvironment.vue

@@ -1,310 +0,0 @@
-<template>
-  <div class="panel lab-environment">
-    <!-- Animated border beam -->
-    <div class="border-beam"></div>
-    <!-- Corner ornaments -->
-    <svg class="pc tl" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
-    <svg class="pc tr" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
-    <svg class="pc bl" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
-    <svg class="pc br" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
-    <!-- Panel header -->
-    <div class="panel-header">
-      <div class="panel-header-icon">&#x1F321;&#xFE0F;</div>
-      <span class="panel-title">实验环境安全智能感知</span>
-      <div class="status-dot red" style="margin-left:auto"></div>
-    </div>
-    <!-- Scrolling sensor list -->
-    <div class="sensor-scroll-wrap">
-      <div class="sensor-scroll-inner">
-        <div
-          v-for="(item, idx) in scrollList"
-          :key="idx"
-          class="sensor-item"
-          :class="{ alert: item.alert }"
-        >
-          <div class="sensor-item-head">
-            <span class="sensor-name">{{ item.name }}({{ item.room }})</span>
-            <span class="sensor-unit">{{ item.unit }}</span>
-            <span v-if="item.alert" class="sensor-status alarm-status">&#x1F6A8; 告警</span>
-            <span v-else class="sensor-status normal-status">&#x25CF; 正常</span>
-          </div>
-          <div class="sensor-metrics">
-            <div class="sensor-metric">&#x1F321;&#xFE0F; {{ item.t }}&deg;C</div>
-            <div class="sensor-metric">&#x1F4A7; {{ item.h }}%</div>
-            <div class="sensor-metric" :class="{ alarm: item.tvoc > 0.6 }">&#x1F9EA; TVOC {{ item.tvoc }}</div>
-            <div class="sensor-metric" :class="{ alarm: item.co2 > 700 }">&#x1F4A8; CO&#x2082; {{ item.co2 }}</div>
-            <div class="sensor-metric">&#x1FAE7; O&#x2082; {{ item.o2 }}%</div>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-import { getSensorList } from '@/api/screen'
-
-export default {
-  name: 'LabEnvironment',
-  data() {
-    return {
-      sensorList: []
-    }
-  },
-  computed: {
-    /** Duplicate data for seamless CSS scroll loop */
-    scrollList() {
-      return [...this.sensorList, ...this.sensorList]
-    }
-  },
-  mounted() {
-    this.fetchData()
-  },
-  methods: {
-    async fetchData() {
-      const res = await getSensorList()
-      if (res.code === 200) {
-        this.sensorList = res.data
-      }
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.lab-environment {
-  position: relative;
-  border-radius: 15px;
-  overflow: hidden;
-  background: $bg-panel;
-  border: 1px solid $border;
-  display: flex;
-  flex-direction: column;
-  height: 100%;
-
-  &::before {
-    content: '';
-    position: absolute;
-    inset: 0;
-    pointer-events: none;
-    border-radius: inherit;
-    background: linear-gradient(135deg, rgba(30,144,255,0.05) 0%, transparent 50%, rgba(0,216,255,0.03) 100%);
-  }
-}
-
-/* ===== Corner ornaments ===== */
-.pc {
-  position: absolute;
-  width: 35px;
-  height: 35px;
-  z-index: 3;
-  pointer-events: none;
-
-  &.tl { top: 0; left: 0; }
-  &.tr { top: 0; right: 0; transform: scaleX(-1); }
-  &.bl { bottom: 0; left: 0; transform: scaleY(-1); }
-  &.br { bottom: 0; right: 0; transform: scale(-1); }
-
-  svg { width: 100%; height: 100%; }
-}
-
-/* ===== Animated border beam ===== */
-.border-beam {
-  position: absolute;
-  inset: 0;
-  pointer-events: none;
-  border-radius: inherit;
-  overflow: hidden;
-  z-index: 2;
-
-  &::before {
-    content: '';
-    position: absolute;
-    top: 0;
-    left: -100%;
-    width: 40%;
-    height: 3px;
-    background: linear-gradient(90deg, transparent, rgba(30,144,255,0.9), rgba(0,216,255,0.7), transparent);
-    animation: beamTop 5s linear infinite;
-  }
-
-  &::after {
-    content: '';
-    position: absolute;
-    bottom: 0;
-    right: -100%;
-    width: 40%;
-    height: 3px;
-    background: linear-gradient(90deg, transparent, rgba(0,216,255,0.7), rgba(30,144,255,0.9), transparent);
-    animation: beamBottom 5s linear infinite 2.5s;
-  }
-}
-
-@keyframes beamTop    { from { left: -40%; }  to { left: 100%; } }
-@keyframes beamBottom { from { right: -40%; } to { right: 100%; } }
-
-/* ===== Panel header ===== */
-.panel-header {
-  display: flex;
-  align-items: center;
-  gap: 25px;
-  padding: 20px 30px 18px;
-  border-bottom: 1px solid $border;
-  background: linear-gradient(90deg, rgba(0,60,160,0.18), transparent);
-  flex-shrink: 0;
-}
-
-.panel-header-icon {
-  width: 65px;
-  height: 65px;
-  border-radius: 12px;
-  flex-shrink: 0;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 32px;
-  background: linear-gradient(135deg, rgba(30,144,255,0.25), rgba(0,216,255,0.15));
-  border: 1px solid rgba(30,144,255,0.35);
-  animation: iconGlow 3s ease-in-out infinite;
-}
-
-@keyframes iconGlow {
-  0%, 100% { box-shadow: 0 0 15px rgba(30,144,255,0.3); }
-  50%      { box-shadow: 0 0 35px rgba(30,144,255,0.7), 0 0 12px rgba(0,216,255,0.3); }
-}
-
-.panel-title {
-  font-size: 30px;
-  font-weight: 600;
-  letter-spacing: 2px;
-  color: $cyan;
-}
-
-/* ===== Status dot ===== */
-.status-dot {
-  width: 20px;
-  height: 20px;
-  border-radius: 50%;
-  display: inline-block;
-
-  &.red {
-    background: $red;
-    box-shadow: 0 0 15px $red;
-    animation: blinkRed 0.8s ease-in-out infinite;
-  }
-
-  &.green {
-    background: $green;
-    box-shadow: 0 0 15px $green;
-    animation: dotPulse 2s ease-in-out infinite;
-  }
-}
-
-@keyframes dotPulse {
-  0%, 100% { transform: scale(1); opacity: 1; }
-  50%      { transform: scale(1.3); opacity: 0.7; }
-}
-
-@keyframes blinkRed {
-  0%, 100% { opacity: 1; }
-  50%      { opacity: 0.35; }
-}
-
-/* ===== Sensor scroll ===== */
-.sensor-scroll-wrap {
-  overflow: hidden;
-  flex: 1;
-  min-height: 0;
-  padding: 15px 20px;
-}
-
-.sensor-scroll-inner {
-  animation: scrollUp 30s linear infinite;
-
-  &:hover {
-    animation-play-state: paused;
-  }
-}
-
-@keyframes scrollUp {
-  0%   { transform: translateY(0); }
-  100% { transform: translateY(-50%); }
-}
-
-/* ===== Sensor item ===== */
-.sensor-item {
-  padding: 18px 20px;
-  border-radius: 10px;
-  margin-bottom: 12px;
-  background: $bg-card;
-  border: 1px solid $border;
-
-  &.alert {
-    border-color: rgba(255,59,59,0.6);
-    background: rgba(255,30,30,0.07);
-    animation: alertGlow 1.5s ease-in-out infinite;
-  }
-}
-
-@keyframes alertGlow {
-  0%, 100% { box-shadow: 0 0 12px rgba(255,59,59,0.25); }
-  50%      { box-shadow: 0 0 40px rgba(255,59,59,0.6); }
-}
-
-.sensor-item-head {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 10px;
-}
-
-.sensor-name {
-  font-size: 28px;
-  font-weight: 600;
-  color: $white;
-}
-
-.sensor-unit {
-  font-size: 25px;
-  color: $text-dim;
-}
-
-.sensor-status {
-  font-size: 25px;
-}
-
-.alarm-status {
-  color: $red;
-  animation: blinkRed 0.8s ease-in-out infinite;
-}
-
-.normal-status {
-  color: $green;
-}
-
-/* ===== Sensor metrics ===== */
-.sensor-metrics {
-  display: flex;
-  gap: 10px;
-  flex-wrap: wrap;
-}
-
-.sensor-metric {
-  padding: 5px 15px;
-  border-radius: 5px;
-  font-size: 25px;
-  background: rgba(4,16,45,0.7);
-  border: 1px solid rgba(30,144,255,0.2);
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  color: $text;
-
-  &.alarm {
-    background: rgba(255,0,30,0.12);
-    border-color: $red;
-    color: $red;
-    animation: blinkRed 0.8s ease-in-out infinite;
-  }
-}
-</style>

+ 0 - 285
src/components/PersonnelTrend.vue

@@ -1,285 +0,0 @@
-<template>
-  <div class="panel personnel-trend">
-    <div class="border-beam"></div>
-    <div class="panel-header">
-      <div class="panel-header-icon">👥</div>
-      <span class="panel-title">实验室进入人数统计及走势</span>
-    </div>
-    <div class="panel-body">
-      <!-- Flip counter digits -->
-      <div class="flip-counters">
-        <div class="flip-counter">
-          <div class="fc-label">今日进入总人数</div>
-          <div class="fc-digits">
-            <div
-              v-for="(digit, idx) in totalDigits"
-              :key="'total-' + idx"
-              class="flip-digit"
-            >{{ digit }}</div>
-          </div>
-        </div>
-        <div class="flip-counter">
-          <div class="fc-label">当前在场实验人数</div>
-          <div class="fc-digits">
-            <div
-              v-for="(digit, idx) in presentDigits"
-              :key="'present-' + idx"
-              class="flip-digit"
-            >{{ digit }}</div>
-          </div>
-        </div>
-      </div>
-      <!-- Line chart -->
-      <div class="chart-wrap">
-        <div ref="chart" class="chart"></div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-import * as echarts from 'echarts'
-import { getPersonnelStats } from '@/api/screen'
-
-export default {
-  name: 'PersonnelTrend',
-  data() {
-    return {
-      totalEntry: 0,
-      currentPresent: 0,
-      displayTotal: 0,
-      displayPresent: 0,
-      chart: null,
-      animTimerTotal: null,
-      animTimerPresent: null
-    }
-  },
-  computed: {
-    totalDigits() {
-      return String(this.displayTotal).padStart(4, '0').split('')
-    },
-    presentDigits() {
-      return String(this.displayPresent).padStart(4, '0').split('')
-    }
-  },
-  mounted() {
-    this.fetchData()
-  },
-  beforeDestroy() {
-    if (this.animTimerTotal) cancelAnimationFrame(this.animTimerTotal)
-    if (this.animTimerPresent) cancelAnimationFrame(this.animTimerPresent)
-    if (this.chart) {
-      this.chart.dispose()
-      this.chart = null
-    }
-  },
-  methods: {
-    async fetchData() {
-      const res = await getPersonnelStats()
-      if (res.code === 200) {
-        this.totalEntry = res.data.totalEntry
-        this.currentPresent = res.data.currentPresent
-
-        // Animated count-up on mount
-        this.$nextTick(() => {
-          setTimeout(() => this.animateCount('total', 0, this.totalEntry, 2000), 600)
-          setTimeout(() => this.animateCount('present', 0, this.currentPresent, 1500), 900)
-          this.initChart(res.data.trend)
-        })
-      }
-    },
-
-    /** Animate count from `from` to `to` over `duration` ms */
-    animateCount(type, from, to, duration) {
-      const startTime = Date.now()
-      const tick = () => {
-        const elapsed = Date.now() - startTime
-        const progress = Math.min(elapsed / duration, 1)
-        const current = Math.round(from + (to - from) * progress)
-        if (type === 'total') {
-          this.displayTotal = current
-        } else {
-          this.displayPresent = current
-        }
-        if (progress < 1) {
-          if (type === 'total') {
-            this.animTimerTotal = requestAnimationFrame(tick)
-          } else {
-            this.animTimerPresent = requestAnimationFrame(tick)
-          }
-        }
-      }
-      tick()
-    },
-
-    /** 初始化面积折线图 - 展示24小时进入/在场人数走势 */
-    initChart(trend) {
-      this.chart = echarts.init(this.$refs.chart, null, {
-        renderer: 'canvas',
-        devicePixelRatio: 2
-      })
-
-      const TOOLTIP_CFG = {
-        backgroundColor: 'rgba(3,14,42,0.92)',
-        borderColor: 'rgba(30,144,255,0.3)',
-        textStyle: { color: '#a8cce8', fontSize: 28 }
-      }
-
-      const option = {
-        backgroundColor: 'transparent',
-        tooltip: {
-          trigger: 'axis',
-          ...TOOLTIP_CFG
-        },
-        legend: {
-          data: ['进入人数', '在场人数'],
-          top: 0,
-          right: 0,
-          textStyle: { color: '#a8cce8', fontSize: 25 },
-          icon: 'circle',
-          itemWidth: 20,
-          itemHeight: 20
-        },
-        grid: {
-          left: 28,
-          right: 8,
-          top: 45,
-          bottom: 50,
-          containLabel: true
-        },
-        xAxis: {
-          type: 'category',
-          data: trend.times,
-          axisLabel: { color: '#5890b8', fontSize: 22 },
-          axisLine: { lineStyle: { color: 'rgba(30,144,255,0.2)' } },
-          axisTick: { show: false }
-        },
-        yAxis: {
-          type: 'value',
-          axisLabel: { color: '#5890b8', fontSize: 22 },
-          axisLine: { show: false },
-          splitLine: { lineStyle: { color: 'rgba(30,144,255,0.1)' } }
-        },
-        series: [
-          {
-            name: '进入人数',
-            type: 'line',
-            data: trend.entry,
-            smooth: true,
-            symbol: 'circle',
-            symbolSize: 12,
-            lineStyle: { color: '#1e90ff', width: 5 },
-            itemStyle: { color: '#1e90ff' },
-            areaStyle: {
-              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-                { offset: 0, color: 'rgba(30,144,255,0.32)' },
-                { offset: 1, color: 'rgba(30,144,255,0.02)' }
-              ])
-            }
-          },
-          {
-            name: '在场人数',
-            type: 'line',
-            data: trend.present,
-            smooth: true,
-            symbol: 'circle',
-            symbolSize: 12,
-            lineStyle: { color: '#ffd740', width: 5 },
-            itemStyle: { color: '#ffd740' },
-            areaStyle: {
-              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-                { offset: 0, color: 'rgba(255,215,64,0.22)' },
-                { offset: 1, color: 'rgba(255,215,64,0.01)' }
-              ])
-            }
-          }
-        ]
-      }
-
-      this.chart.setOption(option)
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.personnel-trend {
-  display: flex;
-  flex-direction: column;
-}
-
-.panel-body {
-  flex: 1;
-  min-height: 0;
-  display: flex;
-  flex-direction: column;
-  padding: 20px 25px;
-}
-
-// Flip counters section
-.flip-counters {
-  display: flex;
-  gap: 25px;
-}
-
-.flip-counter {
-  flex: 1;
-  text-align: center;
-  padding: 18px 15px;
-  border-radius: 10px;
-  background: $bg-card;
-  border: 1px solid $border;
-
-  .fc-label {
-    font-size: 25px;
-    color: $text-dim;
-    margin-bottom: 12px;
-    letter-spacing: 2px;
-  }
-
-  .fc-digits {
-    display: flex;
-    gap: 8px;
-    justify-content: center;
-  }
-}
-
-.flip-digit {
-  width: 50px;
-  height: 75px;
-  border-radius: 8px;
-  background: linear-gradient(180deg, rgba(4, 18, 55, 0.9), rgba(2, 10, 32, 0.95));
-  border: 1px solid rgba(30, 144, 255, 0.3);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 45px;
-  font-weight: 700;
-  color: $gold;
-  font-variant-numeric: tabular-nums;
-  position: relative;
-  overflow: hidden;
-
-  &::after {
-    content: '';
-    position: absolute;
-    left: 0;
-    right: 0;
-    top: 50%;
-    height: 2px;
-    background: rgba(0, 0, 0, 0.4);
-  }
-}
-
-// Chart area
-.chart-wrap {
-  flex: 1;
-  min-height: 0;
-  margin-top: 12px;
-
-  .chart {
-    width: 100%;
-    height: 100%;
-  }
-}
-</style>

+ 0 - 165
src/components/SafetyCompliance.vue

@@ -1,165 +0,0 @@
-<template>
-  <div class="panel safety-compliance">
-    <div class="border-beam"></div>
-    <div class="panel-header">
-      <div class="panel-header-icon">📊</div>
-      <span class="panel-title">实验室安全分级统计</span>
-    </div>
-    <div class="chart-wrap">
-      <div ref="chart" class="chart"></div>
-    </div>
-  </div>
-</template>
-
-<script>
-import * as echarts from 'echarts'
-import { getSafetyLevelStats } from '@/api/screen'
-
-export default {
-  name: 'SafetyCompliance',
-  data() {
-    return {
-      chart: null,
-      scrollTimer: null,
-      scrollIndex: 0
-    }
-  },
-  mounted() {
-    this.fetchData()
-  },
-  beforeDestroy() {
-    if (this.scrollTimer) {
-      clearInterval(this.scrollTimer)
-      this.scrollTimer = null
-    }
-    if (this.chart) {
-      this.chart.dispose()
-      this.chart = null
-    }
-  },
-  methods: {
-    async fetchData() {
-      const res = await getSafetyLevelStats()
-      if (res.code === 200) {
-        this.$nextTick(() => this.initChart(res.data))
-      }
-    },
-    /** 初始化堆叠柱状图 - 展示各单位实验室安全分级统计 */
-    initChart(data) {
-      this.chart = echarts.init(this.$refs.chart, null, {
-        renderer: 'canvas',
-        devicePixelRatio: 2
-      })
-
-      const { levels, colors, units, totals, series: seriesData } = data
-
-      // X-axis labels: "unitName\n(total)"
-      const xLabels = units.map((u, i) => `${u}\n(${totals[i]})`)
-
-      const TOOLTIP_CFG = {
-        backgroundColor: 'rgba(3,14,42,0.92)',
-        borderColor: 'rgba(30,144,255,0.3)',
-        textStyle: { color: '#a8cce8', fontSize: 28 }
-      }
-
-      const option = {
-        backgroundColor: 'transparent',
-        tooltip: {
-          trigger: 'axis',
-          axisPointer: { type: 'shadow' },
-          ...TOOLTIP_CFG
-        },
-        legend: {
-          data: levels,
-          top: 0,
-          right: 0,
-          textStyle: { color: '#a8cce8', fontSize: 25 },
-          icon: 'rect',
-          itemWidth: 25,
-          itemHeight: 15
-        },
-        grid: {
-          left: 30,
-          right: 8,
-          top: 55,
-          bottom: 110,
-          containLabel: true
-        },
-        xAxis: {
-          type: 'category',
-          data: xLabels,
-          axisLabel: {
-            color: '#5890b8',
-            fontSize: 22,
-            interval: 0,
-            lineHeight: 35
-          },
-          axisLine: { lineStyle: { color: 'rgba(30,144,255,0.2)' } },
-          axisTick: { show: false }
-        },
-        yAxis: {
-          type: 'value',
-          name: '间',
-          nameTextStyle: { color: '#5890b8', fontSize: 25 },
-          axisLabel: { color: '#5890b8', fontSize: 25 },
-          axisLine: { show: false },
-          splitLine: { lineStyle: { color: 'rgba(30,144,255,0.1)' } }
-        },
-        dataZoom: [{
-          type: 'inside',
-          startValue: 0,
-          endValue: 5
-        }],
-        series: levels.map((name, i) => ({
-          name: name,
-          type: 'bar',
-          stack: 'total',
-          barMaxWidth: 55,
-          data: seriesData[i].map(v => ({
-            value: v,
-            itemStyle: { color: colors[i], opacity: 0.88 }
-          })),
-          emphasis: { itemStyle: { opacity: 1 } }
-        }))
-      }
-
-      this.chart.setOption(option)
-
-      // Auto-scroll dataZoom every 5 seconds
-      this.scrollIndex = 0
-      this.scrollTimer = setInterval(() => {
-        this.scrollIndex++
-        if (this.scrollIndex >= units.length - 5) {
-          this.scrollIndex = -1
-        }
-        if (this.chart) {
-          this.chart.dispatchAction({
-            type: 'dataZoom',
-            startValue: this.scrollIndex,
-            endValue: this.scrollIndex + 5
-          })
-        }
-      }, 5000)
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.safety-compliance {
-  display: flex;
-  flex-direction: column;
-  height: 100%;
-}
-
-.chart-wrap {
-  flex: 1;
-  min-height: 0;
-  padding: 15px 20px;
-
-  .chart {
-    width: 100%;
-    height: 100%;
-  }
-}
-</style>

+ 0 - 278
src/components/ScreenHeader.vue

@@ -1,278 +0,0 @@
-<template>
-  <div class="screen-header">
-    <!-- Left: Clock -->
-    <div class="nav-left">
-      <div class="nav-clock">
-        <div class="time">{{ currentTime }}</div>
-        <div class="date">{{ currentDate }}</div>
-      </div>
-    </div>
-
-    <!-- Center: Logo + Org + Divider + Title -->
-    <div class="nav-center">
-      <div class="nav-logo">
-        <div class="nav-logo-ring"></div>
-      </div>
-      <div class="nav-org">
-        <strong>中国安全生产科学研究院</strong>
-        <span>National Institute for Occupational Safety</span>
-      </div>
-      <div class="nav-divider"></div>
-      <div class="nav-title-wrap">
-        <div class="nav-title">实验室安全智能监测与管控中心</div>
-        <div class="nav-title-deco"></div>
-      </div>
-    </div>
-
-    <!-- Right: Weather -->
-    <div class="nav-right">
-      <div class="nav-weather">
-        <span class="icon">&#9925;</span>
-        <div>
-          <div class="weather-location">北京 &middot; 晴转多云</div>
-          <div class="weather-detail">12&deg;C / AQI 68</div>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-export default {
-  name: 'ScreenHeader',
-  data() {
-    return {
-      currentTime: '',
-      currentDate: '',
-      timer: null
-    }
-  },
-  mounted() {
-    this.updateTime()
-    this.timer = setInterval(this.updateTime, 1000)
-  },
-  beforeDestroy() {
-    if (this.timer) {
-      clearInterval(this.timer)
-      this.timer = null
-    }
-  },
-  methods: {
-    updateTime() {
-      const now = new Date()
-      const h = String(now.getHours()).padStart(2, '0')
-      const m = String(now.getMinutes()).padStart(2, '0')
-      const s = String(now.getSeconds()).padStart(2, '0')
-      this.currentTime = `${h}:${m}:${s}`
-
-      const y = now.getFullYear()
-      const mon = now.getMonth() + 1
-      const d = now.getDate()
-      const weekDays = ['日', '一', '二', '三', '四', '五', '六']
-      this.currentDate = `${y}年${mon}月${d}日 星期${weekDays[now.getDay()]}`
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-@import '../styles/variables.scss';
-
-/* ========== ringPulse: logo outer ring breathing animation ========== */
-@keyframes ringPulse {
-  0%, 100% {
-    transform: scale(1);
-    opacity: 0.5;
-  }
-  50% {
-    transform: scale(1.1);
-    opacity: 1;
-  }
-}
-
-/* ========== titleShine: title gradient scroll animation ========== */
-@keyframes titleShine {
-  from {
-    background-position: 0% center;
-  }
-  to {
-    background-position: 200% center;
-  }
-}
-
-/* ========== decoGlow: decorative line glow animation ========== */
-@keyframes decoGlow {
-  0%, 100% {
-    opacity: 0.5;
-  }
-  50% {
-    opacity: 1;
-    filter: blur(1px);
-  }
-}
-
-/* ========== TOP NAV ========== */
-.screen-header {
-  position: relative;
-  z-index: 10;
-  flex-shrink: 0;
-  width: 100%;
-  height: 160px;
-  display: flex;
-  align-items: stretch;
-  background: linear-gradient(180deg, rgba(4, 18, 52, 0.99) 0%, rgba(3, 12, 36, 0.97) 100%);
-  border-bottom: 1px solid rgba(30, 144, 255, 0.3);
-  box-shadow: 0 2px 30px rgba(0, 80, 220, 0.28), inset 0 1px 0 rgba(30, 144, 255, 0.15);
-  font-family: $font-main;
-}
-
-/* ========== LEFT: Clock ========== */
-.nav-left {
-  display: flex;
-  align-items: center;
-  padding: 0 50px;
-  width: 950px;
-  gap: 30px;
-}
-
-.nav-clock {
-  .time {
-    font-size: 55px;
-    font-weight: 300;
-    color: $gold;
-    font-variant-numeric: tabular-nums;
-    letter-spacing: 4px;
-    line-height: 1;
-  }
-
-  .date {
-    font-size: 30px;
-    color: $text-dim;
-    margin-top: 5px;
-  }
-}
-
-/* ========== CENTER: Logo + Org + Divider + Title ========== */
-.nav-center {
-  flex: 1;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  gap: 30px;
-  position: relative;
-}
-
-.nav-logo {
-  width: 100px;
-  height: 100px;
-  border-radius: 20px;
-  flex-shrink: 0;
-  background: linear-gradient(135deg, #0d2d6b, #1565c0);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  box-shadow: 0 0 40px rgba(30, 144, 255, 0.6), inset 0 0 20px rgba(255, 255, 255, 0.1);
-  position: relative;
-  overflow: visible;
-
-  &::after {
-    content: '安';
-    font-size: 45px;
-    font-weight: 900;
-    color: rgba(255, 255, 255, 0.95);
-  }
-}
-
-.nav-logo-ring {
-  position: absolute;
-  inset: -10px;
-  border-radius: 25px;
-  border: 2px solid rgba(30, 144, 255, 0.5);
-  animation: ringPulse 2s ease-in-out infinite;
-}
-
-.nav-org {
-  font-size: 30px;
-  color: $text-dim;
-  line-height: 1.4;
-  margin-right: 50px;
-
-  strong {
-    display: block;
-    font-size: 32px;
-    color: $cyan;
-  }
-}
-
-.nav-divider {
-  width: 2px;
-  height: 80px;
-  background: linear-gradient(180deg, transparent, rgba(30, 144, 255, 0.6), transparent);
-  flex-shrink: 0;
-  margin-right: 50px;
-}
-
-.nav-title-wrap {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  position: relative;
-}
-
-.nav-title {
-  font-size: 65px;
-  font-weight: 700;
-  letter-spacing: 10px;
-  background: linear-gradient(90deg, $blue, $cyan, $white, $cyan, $blue);
-  background-size: 200% auto;
-  -webkit-background-clip: text;
-  -webkit-text-fill-color: transparent;
-  background-clip: text;
-  animation: titleShine 4s linear infinite;
-  filter: drop-shadow(0 0 40px rgba(30, 144, 255, 0.5));
-  white-space: nowrap;
-}
-
-.nav-title-deco {
-  position: absolute;
-  bottom: -5px;
-  left: 50%;
-  transform: translateX(-50%);
-  width: 900px;
-  height: 2px;
-  background: linear-gradient(90deg, transparent, $blue, $cyan, $blue, transparent);
-  animation: decoGlow 3s ease-in-out infinite;
-}
-
-/* ========== RIGHT: Weather ========== */
-.nav-right {
-  width: 950px;
-  display: flex;
-  align-items: center;
-  justify-content: flex-end;
-  padding: 0 50px;
-  gap: 50px;
-}
-
-.nav-weather {
-  display: flex;
-  align-items: center;
-  gap: 20px;
-  font-size: 32px;
-  color: $text-dim;
-
-  .icon {
-    font-size: 45px;
-  }
-
-  .weather-location {
-    color: $text-dim;
-  }
-
-  .weather-detail {
-    font-size: 28px;
-    color: $gold;
-    margin-top: 4px;
-  }
-}
-</style>

+ 0 - 593
src/components/SecurityMonitor.vue

@@ -1,593 +0,0 @@
-<template>
-  <div class="security-monitor panel">
-    <!-- Monitor Header -->
-    <div class="monitor-header">
-      <span class="monitor-title-cn">&#x1F4F9; &#x5B9E;&#x65F6;&#x76D1;&#x63A7;</span>
-      <span class="monitor-title-en">CCTV Live Feed</span>
-      <div class="monitor-status">
-        <div class="status-dot green"></div>
-        <span class="monitor-status-text">&#x4FE1;&#x53F7;&#x6B63;&#x5E38;</span>
-      </div>
-    </div>
-
-    <!-- Monitor Inner: 2-column grid -->
-    <div class="monitor-inner">
-      <!-- Left Column: Search + Filter + Tree -->
-      <div class="monitor-left">
-        <div class="search-box">
-          <span class="search-icon">&#x1F50D;</span>
-          <input
-            v-model="searchText"
-            type="text"
-            placeholder="&#x641C;&#x7D22;&#x697C;&#x680B; / &#x697C;&#x5C42; / &#x623F;&#x95F4;&#x2026;"
-          />
-        </div>
-
-        <select v-model="selectedUnit" class="filter-select">
-          <option value="">&#x5168;&#x90E8;&#x4E8C;&#x7EA7;&#x5355;&#x4F4D;</option>
-          <option>&#x5316;&#x5B66;&#x7814;&#x7A76;&#x6240;</option>
-          <option>&#x7269;&#x7406;&#x7814;&#x7A76;&#x6240;</option>
-          <option>&#x751F;&#x7269;&#x7814;&#x7A76;&#x6240;</option>
-          <option>&#x6750;&#x6599;&#x7814;&#x7A76;&#x6240;</option>
-          <option>&#x5DE5;&#x7A0B;&#x7814;&#x7A76;&#x6240;</option>
-        </select>
-
-        <div class="tree-wrap">
-          <tree-node
-            v-if="treeData"
-            :node="treeData"
-            :depth="0"
-            :selected-label="selectedTreeLabel"
-            @select="onTreeNodeSelect"
-          />
-        </div>
-      </div>
-
-      <!-- Right Column: Breadcrumb + Pager + Camera Grid -->
-      <div class="monitor-right">
-        <div class="camera-grid-header">
-          <div class="camera-breadcrumb">
-            <span>&#x5B89;&#x79D1;&#x9662;&#x4E3B;&#x56ED;&#x533A;</span> &#x203A;
-            <span>&#x7EFC;&#x5408;&#x5B9E;&#x9A8C;&#x697C;A</span> &#x203A;
-            <span>3&#x5C42;</span>
-          </div>
-          <div class="camera-pager">
-            <button class="pager-btn" @click="prevPage">&lsaquo;</button>
-            <span class="pager-info">{{ currentPage }} / {{ totalPages }} &#x9875;</span>
-            <button class="pager-btn" @click="nextPage">&rsaquo;</button>
-          </div>
-        </div>
-
-        <div class="camera-grid">
-          <div
-            v-for="(cam, idx) in cameras"
-            :key="'cam-' + idx"
-            class="camera-cell"
-            :class="{ 'ai-cam': idx === 0 }"
-          >
-            <!-- AI badge (first cell only) -->
-            <div v-if="idx === 0" class="camera-ai-badge">&#x1F916; AI&#x68C0;&#x6D4B;</div>
-
-            <!-- REC indicator -->
-            <div class="camera-rec">REC</div>
-
-            <!-- Video placeholder -->
-            <div class="camera-placeholder-text">CCTV</div>
-
-            <!-- AI detection box (first cell only) -->
-            <div v-if="idx === 0" class="ai-detection-box" style="left:28%;top:18%;width:22%;height:38%">
-              <div class="ai-detection-label">&#x5371;&#x9669;&#x884C;&#x4E3A;: &#x672A;&#x4F69;&#x6234;&#x9632;&#x62A4;</div>
-            </div>
-
-            <!-- Camera label -->
-            <div class="camera-label">{{ cam }}</div>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-import { getCameraList } from '@/api/screen'
-
-const TreeNode = {
-  name: 'TreeNode',
-  props: {
-    node: { type: Object, required: true },
-    depth: { type: Number, default: 0 },
-    selectedLabel: { type: String, default: '' }
-  },
-  data() {
-    return {
-      expanded: this.depth === 0
-    }
-  },
-  computed: {
-    isLeaf() {
-      return !this.node.children || this.node.children.length === 0
-    },
-    icon() {
-      const icons = ['\u{1F3E2}', '\u{1F3D7}\uFE0F', '\u{1F4D0}', '\u{1F52C}']
-      return icons[Math.min(this.depth, 3)]
-    },
-    isSelected() {
-      return this.node.label === this.selectedLabel
-    }
-  },
-  methods: {
-    toggle() {
-      if (!this.isLeaf) {
-        this.expanded = !this.expanded
-      }
-      this.$emit('select', this.node.label)
-    },
-    onChildSelect(label) {
-      this.$emit('select', label)
-    }
-  },
-  template: `
-    <div class="tree-node">
-      <div
-        class="tree-node-label"
-        :class="{ selected: isSelected }"
-        @click="toggle"
-      >
-        <span class="arrow" :class="{ open: expanded }">
-          <template v-if="!isLeaf">&#x25B6;</template>
-          <template v-else>&nbsp;&nbsp;</template>
-        </span>
-        <span>{{ icon }}</span>
-        <span>{{ node.label }}</span>
-      </div>
-      <div v-if="!isLeaf" class="tree-children" :class="{ open: expanded }">
-        <tree-node
-          v-for="(child, i) in node.children"
-          :key="i"
-          :node="child"
-          :depth="depth + 1"
-          :selected-label="selectedLabel"
-          @select="onChildSelect"
-        />
-      </div>
-    </div>
-  `
-}
-
-export default {
-  name: 'SecurityMonitor',
-  components: {
-    TreeNode
-  },
-  data() {
-    return {
-      searchText: '',
-      selectedUnit: '',
-      selectedTreeLabel: '',
-      currentPage: 1,
-      totalPages: 3,
-      cameras: [],
-      treeData: {
-        label: '\u5B89\u79D1\u9662\u4E3B\u56ED\u533A',
-        children: [
-          {
-            label: '\u7EFC\u5408\u5B9E\u9A8C\u697CA',
-            children: [
-              { label: '1\u5C42', children: [{ label: 'A101 \u5316\u5B66\u5B9E\u9A8C\u5BA4' }, { label: 'A102 \u5206\u6790\u5BA4' }] },
-              { label: '2\u5C42', children: [{ label: 'A201 \u751F\u7269\u5B9E\u9A8C\u5BA4' }] },
-              { label: '3\u5C42', children: [{ label: 'A301 \u6709\u673A\u5408\u6210\u5BA4' }, { label: 'A302 \u6838\u78C1\u5171\u632F\u5BA4' }, { label: 'A303 \u8D28\u8C31\u5BA4' }] },
-              { label: '4\u5C42', children: [{ label: 'A401 X\u5C04\u7EBF\u5BA4' }] }
-            ]
-          },
-          {
-            label: '\u7269\u7406\u5B9E\u9A8C\u697CB',
-            children: [
-              { label: '1\u5C42', children: [{ label: 'B101 \u5149\u5B66\u5B9E\u9A8C\u5BA4' }] },
-              { label: '2\u5C42', children: [{ label: 'B201 \u4F4E\u6E29\u5B9E\u9A8C\u5BA4' }] }
-            ]
-          },
-          {
-            label: '\u5DE5\u7A0B\u6280\u672F\u697CC',
-            children: [
-              { label: '1\u5C42', children: [{ label: 'C101 \u673A\u68B0\u52A0\u5DE5\u5BA4' }] },
-              { label: '2\u5C42', children: [{ label: 'C201 \u6750\u6599\u6D4B\u8BD5\u5BA4' }] }
-            ]
-          }
-        ]
-      }
-    }
-  },
-  created() {
-    this.fetchCameras()
-  },
-  methods: {
-    async fetchCameras() {
-      try {
-        const res = await getCameraList()
-        if (res.code === 200) {
-          this.cameras = res.data
-        }
-      } catch (e) {
-        // fallback to static data
-        this.cameras = [
-          'A301 \u6709\u673A\u5408\u6210\u5BA4',
-          'A302 \u6838\u78C1\u5171\u632F\u5BA4',
-          'A303 \u8D28\u8C31\u5BA4',
-          'A301 \u8D70\u5ECA',
-          'A302 \u8D70\u5ECA',
-          'A303 \u8D70\u5ECA',
-          'A\u5C42\u516C\u5171\u533A\u57DF',
-          'A\u5C42\u5B89\u5168\u901A\u9053',
-          'A\u5C42\u51FA\u5165\u53E3'
-        ]
-      }
-    },
-    onTreeNodeSelect(label) {
-      this.selectedTreeLabel = label
-    },
-    prevPage() {
-      if (this.currentPage > 1) {
-        this.currentPage--
-      }
-    },
-    nextPage() {
-      if (this.currentPage < this.totalPages) {
-        this.currentPage++
-      }
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.security-monitor {
-  display: flex;
-  flex-direction: column;
-  height: 100%;
-  min-height: 0;
-}
-
-// ========== Monitor Header ==========
-.monitor-header {
-  padding: 20px 30px;
-  border-bottom: 1px solid $border;
-  flex-shrink: 0;
-  display: flex;
-  align-items: center;
-  gap: 25px;
-  background: linear-gradient(90deg, rgba(0, 60, 160, 0.18), transparent);
-}
-
-.monitor-title-cn {
-  font-size: 32px;
-  font-weight: 600;
-  color: $cyan;
-  letter-spacing: 2px;
-}
-
-.monitor-title-en {
-  font-size: 28px;
-  color: $text-dim;
-  margin-left: 10px;
-}
-
-.monitor-status {
-  margin-left: auto;
-  display: flex;
-  align-items: center;
-  gap: 15px;
-}
-
-.monitor-status-text {
-  font-size: 28px;
-  color: $text-dim;
-}
-
-// ========== Monitor Inner (2-column grid) ==========
-.monitor-inner {
-  flex: 1;
-  display: grid;
-  grid-template-columns: 575px 1fr;
-  gap: 0;
-  min-height: 0;
-  overflow: hidden;
-}
-
-// ========== Left Column ==========
-.monitor-left {
-  display: flex;
-  flex-direction: column;
-  gap: 15px;
-  padding: 20px;
-  border-right: 1px solid $border;
-  min-height: 0;
-  overflow: hidden;
-}
-
-.search-box {
-  display: flex;
-  align-items: center;
-  gap: 18px;
-  padding: 15px 25px;
-  border-radius: 10px;
-  background: $bg-card;
-  border: 1px solid $border;
-  flex-shrink: 0;
-
-  .search-icon {
-    font-size: 32px;
-    color: $text-dim;
-  }
-
-  input {
-    flex: 1;
-    background: none;
-    border: none;
-    outline: none;
-    color: $text;
-    font-size: 30px;
-    font-family: inherit;
-
-    &::placeholder {
-      color: $text-dim;
-    }
-  }
-}
-
-.filter-select {
-  padding: 12px 25px;
-  border-radius: 10px;
-  width: 100%;
-  flex-shrink: 0;
-  background: $bg-card;
-  border: 1px solid $border;
-  color: $text;
-  font-size: 28px;
-  font-family: inherit;
-  outline: none;
-  cursor: pointer;
-  appearance: none;
-  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M0 0l6 8 6-8z' fill='rgba(30,144,255,0.6)'/%3E%3C/svg%3E");
-  background-repeat: no-repeat;
-  background-position: right 25px center;
-}
-
-// ========== Tree ==========
-.tree-wrap {
-  flex: 1;
-  overflow-y: auto;
-  overflow-x: hidden;
-  min-height: 0;
-
-  &::-webkit-scrollbar {
-    width: 8px;
-  }
-
-  &::-webkit-scrollbar-thumb {
-    background: $border;
-    border-radius: 5px;
-  }
-}
-
-::v-deep .tree-node-label {
-  display: flex;
-  align-items: center;
-  gap: 15px;
-  padding: 12px 20px;
-  border-radius: 8px;
-  font-size: 28px;
-  color: $text;
-  transition: all 0.2s;
-  user-select: none;
-  cursor: pointer;
-
-  &:hover {
-    background: $blue-dim;
-    color: $blue;
-  }
-
-  &.selected {
-    background: rgba(30, 144, 255, 0.15);
-    color: $cyan;
-    border-left: 5px solid $blue;
-  }
-
-  .arrow {
-    transition: transform 0.2s;
-    font-size: 25px;
-    color: $text-dim;
-    flex-shrink: 0;
-
-    &.open {
-      transform: rotate(90deg);
-    }
-  }
-}
-
-::v-deep .tree-children {
-  padding-left: 35px;
-  display: none;
-
-  &.open {
-    display: block;
-  }
-}
-
-// ========== Right Column ==========
-.monitor-right {
-  padding: 20px;
-  display: flex;
-  flex-direction: column;
-  gap: 15px;
-  min-height: 0;
-}
-
-.camera-grid-header {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  flex-shrink: 0;
-}
-
-.camera-breadcrumb {
-  font-size: 30px;
-  color: $text-dim;
-
-  span {
-    color: $cyan;
-  }
-}
-
-.camera-pager {
-  display: flex;
-  align-items: center;
-  gap: 15px;
-}
-
-.pager-btn {
-  width: 60px;
-  height: 60px;
-  border-radius: 8px;
-  cursor: pointer;
-  background: $bg-card;
-  border: 1px solid $border;
-  color: $text-dim;
-  font-size: 32px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  transition: all 0.2s;
-
-  &:hover {
-    border-color: $blue;
-    color: $blue;
-  }
-}
-
-.pager-info {
-  font-size: 28px;
-  color: $text-dim;
-}
-
-// ========== Camera Grid (3x3) ==========
-.camera-grid {
-  display: grid;
-  grid-template-columns: repeat(3, 1fr);
-  grid-template-rows: repeat(3, 1fr);
-  gap: 12px;
-  flex: 1;
-  min-height: 0;
-}
-
-.camera-cell {
-  position: relative;
-  border-radius: 10px;
-  overflow: hidden;
-  background: #020810;
-  border: 1px solid $border;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-
-  &:hover {
-    border-color: $blue;
-  }
-
-  &.ai-cam {
-    border-color: rgba(0, 230, 118, 0.4);
-  }
-}
-
-.camera-placeholder-text {
-  font-size: 48px;
-  color: rgba(110, 165, 210, 0.15);
-  letter-spacing: 8px;
-  font-weight: 700;
-  user-select: none;
-}
-
-.camera-label {
-  position: absolute;
-  bottom: 10px;
-  left: 15px;
-  font-size: 25px;
-  color: rgba(255, 255, 255, 0.75);
-  background: rgba(0, 0, 0, 0.5);
-  padding: 3px 10px;
-  border-radius: 5px;
-}
-
-.camera-rec {
-  position: absolute;
-  top: 10px;
-  right: 15px;
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  font-size: 25px;
-  color: $red;
-  background: rgba(0, 0, 0, 0.5);
-  padding: 3px 10px;
-  border-radius: 5px;
-
-  &::before {
-    content: '';
-    width: 12px;
-    height: 12px;
-    border-radius: 50%;
-    background: $red;
-    animation: blinkRed 1s ease-in-out infinite;
-  }
-}
-
-.camera-ai-badge {
-  position: absolute;
-  top: 10px;
-  left: 15px;
-  font-size: 25px;
-  color: $green;
-  background: rgba(0, 30, 15, 0.75);
-  padding: 3px 10px;
-  border-radius: 5px;
-  border: 1px solid rgba(0, 230, 118, 0.3);
-  z-index: 2;
-}
-
-.ai-detection-box {
-  position: absolute;
-  border: 5px solid $red;
-  background: rgba(255, 59, 59, 0.1);
-  border-radius: 5px;
-  animation: detBox 1.5s ease-in-out infinite;
-}
-
-.ai-detection-label {
-  position: absolute;
-  bottom: 100%;
-  left: 0;
-  white-space: nowrap;
-  font-size: 25px;
-  background: $red;
-  color: #fff;
-  padding: 3px 10px;
-  border-radius: 5px;
-  margin-bottom: 5px;
-}
-
-// ========== Keyframes ==========
-@keyframes blinkRed {
-  0%, 100% { opacity: 1; }
-  50% { opacity: 0.35; }
-}
-
-@keyframes detBox {
-  0%, 100% {
-    border-color: #ff3b3b;
-    box-shadow: 0 0 20px rgba(255, 59, 59, 0.4);
-  }
-  50% {
-    border-color: #fca5a5;
-    box-shadow: 0 0 45px rgba(255, 59, 59, 0.7);
-  }
-}
-</style>

+ 0 - 21
src/main.js

@@ -1,21 +0,0 @@
-import Vue from 'vue'
-import App from './App.vue'
-import router from './router'
-import store from './store'
-
-// 完整引入 Element UI
-import ElementUI from 'element-ui'
-import 'element-ui/lib/theme-chalk/index.css'
-
-// 全局样式
-import '@/styles/global.scss'
-
-Vue.use(ElementUI)
-
-Vue.config.productionTip = false
-
-new Vue({
-  router,
-  store,
-  render: h => h(App)
-}).$mount('#app')

+ 0 - 40
src/router/index.js

@@ -1,40 +0,0 @@
-import Vue from 'vue'
-import VueRouter from 'vue-router'
-
-Vue.use(VueRouter)
-
-const routes = [
-  {
-    path: '/',
-    redirect: '/login'
-  },
-  {
-    path: '/login',
-    name: 'Login',
-    component: () => import('@/views/Login.vue')
-  },
-  {
-    path: '/screen',
-    name: 'Screen',
-    component: () => import('@/views/Screen.vue'),
-    meta: { requiresAuth: true }
-  }
-]
-
-const router = new VueRouter({
-  mode: 'history',
-  base: process.env.BASE_URL,
-  routes
-})
-
-// 路由守卫:未登录跳转到登录页
-router.beforeEach((to, from, next) => {
-  const token = localStorage.getItem('token')
-  if (to.meta.requiresAuth && !token) {
-    next('/login')
-  } else {
-    next()
-  }
-})
-
-export default router

+ 0 - 31
src/store/index.js

@@ -1,31 +0,0 @@
-import Vue from 'vue'
-import Vuex from 'vuex'
-
-Vue.use(Vuex)
-
-export default new Vuex.Store({
-  state: {
-    token: localStorage.getItem('token') || ''
-  },
-  mutations: {
-    SET_TOKEN(state, token) {
-      state.token = token
-      localStorage.setItem('token', token)
-    },
-    CLEAR_TOKEN(state) {
-      state.token = ''
-      localStorage.removeItem('token')
-    }
-  },
-  actions: {
-    login({ commit }, token) {
-      commit('SET_TOKEN', token)
-    },
-    logout({ commit }) {
-      commit('CLEAR_TOKEN')
-    }
-  },
-  getters: {
-    isAuthenticated: state => !!state.token
-  }
-})

+ 0 - 161
src/styles/global.scss

@@ -1,161 +0,0 @@
-// 全局样式 - 匹配 index-v2.html
-
-*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
-
-html, body {
-  width: 100%;
-  height: 100%;
-  overflow: auto;
-  background: $bg-deep;
-  font-family: $font-main;
-  color: $text;
-}
-
-// 滚动条
-::-webkit-scrollbar { width: 8px; height: 8px; }
-::-webkit-scrollbar-track { background: transparent; }
-::-webkit-scrollbar-thumb { background: $border; border-radius: 5px; }
-
-// ========== 面板系统 ==========
-.panel {
-  position: relative;
-  border-radius: 15px;
-  overflow: hidden;
-  background: $bg-panel;
-  border: 1px solid $border;
-
-  &::before {
-    content: '';
-    position: absolute;
-    inset: 0;
-    pointer-events: none;
-    border-radius: inherit;
-    background: linear-gradient(135deg, rgba(30, 144, 255, 0.05) 0%, transparent 50%, rgba(0, 216, 255, 0.03) 100%);
-  }
-}
-
-// 角标装饰
-.pc {
-  position: absolute;
-  width: 35px;
-  height: 35px;
-  z-index: 3;
-  pointer-events: none;
-
-  &.tl { top: 0; left: 0; }
-  &.tr { top: 0; right: 0; transform: scaleX(-1); }
-  &.bl { bottom: 0; left: 0; transform: scaleY(-1); }
-  &.br { bottom: 0; right: 0; transform: scale(-1); }
-
-  svg { width: 100%; height: 100%; }
-}
-
-// 光束边框动画
-.border-beam {
-  position: absolute;
-  inset: 0;
-  pointer-events: none;
-  border-radius: inherit;
-  overflow: hidden;
-  z-index: 2;
-
-  &::before {
-    content: '';
-    position: absolute;
-    top: 0;
-    left: -100%;
-    width: 40%;
-    height: 3px;
-    background: linear-gradient(90deg, transparent, rgba(30, 144, 255, 0.9), rgba(0, 216, 255, 0.7), transparent);
-    animation: beamTop 5s linear infinite;
-  }
-
-  &::after {
-    content: '';
-    position: absolute;
-    bottom: 0;
-    right: -100%;
-    width: 40%;
-    height: 3px;
-    background: linear-gradient(90deg, transparent, rgba(0, 216, 255, 0.7), rgba(30, 144, 255, 0.9), transparent);
-    animation: beamBottom 5s linear infinite 2.5s;
-  }
-}
-
-@keyframes beamTop { from { left: -40%; } to { left: 100%; } }
-@keyframes beamBottom { from { right: -40%; } to { right: 100%; } }
-
-// 面板头部
-.panel-header {
-  display: flex;
-  align-items: center;
-  gap: 25px;
-  padding: 20px 30px 18px;
-  border-bottom: 1px solid $border;
-  background: linear-gradient(90deg, rgba(0, 60, 160, 0.18), transparent);
-  flex-shrink: 0;
-}
-
-.panel-header-icon {
-  width: 65px;
-  height: 65px;
-  border-radius: 12px;
-  flex-shrink: 0;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 32px;
-  background: linear-gradient(135deg, rgba(30, 144, 255, 0.25), rgba(0, 216, 255, 0.15));
-  border: 1px solid rgba(30, 144, 255, 0.35);
-  animation: iconGlow 3s ease-in-out infinite;
-}
-
-@keyframes iconGlow {
-  0%, 100% { box-shadow: 0 0 15px rgba(30, 144, 255, 0.3); }
-  50% { box-shadow: 0 0 35px rgba(30, 144, 255, 0.7), 0 0 12px rgba(0, 216, 255, 0.3); }
-}
-
-.panel-title {
-  font-size: 30px;
-  font-weight: 600;
-  letter-spacing: 2px;
-  color: $cyan;
-}
-
-// 扫描线效果
-.scan-effect {
-  position: absolute;
-  inset: 0;
-  pointer-events: none;
-  overflow: hidden;
-  z-index: 0;
-
-  &::after {
-    content: '';
-    position: absolute;
-    left: 0;
-    right: 0;
-    height: 200px;
-    top: -200px;
-    background: linear-gradient(180deg, transparent, rgba(30, 144, 255, 0.025), transparent);
-    animation: scanFull 9s linear infinite;
-  }
-}
-
-@keyframes scanFull { from { top: -200px; } to { top: 100%; } }
-
-// 状态圆点
-.status-dot {
-  width: 20px;
-  height: 20px;
-  border-radius: 50%;
-  display: inline-block;
-  animation: dotPulse 2s ease-in-out infinite;
-
-  &.green { background: $green; box-shadow: 0 0 15px $green; }
-  &.red { background: $red; box-shadow: 0 0 15px $red; animation: blinkRed 0.8s ease-in-out infinite; }
-  &.orange { background: #f59e0b; box-shadow: 0 0 15px #f59e0b; }
-}
-
-@keyframes dotPulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.3); opacity: 0.7; } }
-@keyframes blinkRed { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

+ 0 - 37
src/styles/variables.scss

@@ -1,37 +0,0 @@
-// Sass 全局变量 - 匹配 index-v2.html 深蓝科幻主题
-
-// 主背景色
-$bg-deep: #030e1f;
-$bg-panel: rgba(5, 16, 42, 0.95);
-$bg-card: rgba(7, 22, 54, 0.85);
-
-// 主色
-$blue: #1e90ff;
-$blue-dim: rgba(30, 144, 255, 0.14);
-$blue-glow: rgba(30, 144, 255, 0.5);
-$cyan: #00d8ff;
-$cyan-dim: rgba(0, 216, 255, 0.12);
-$teal: #00e5c8;
-$indigo: #4361ee;
-$red: #ff3b3b;
-$green: #00e676;
-$gold: #ffd740;
-
-// 文字
-$white: #ddf0ff;
-$text: #a8cce8;
-$text-dim: rgba(110, 165, 210, 0.55);
-
-// 边框
-$border: rgba(30, 144, 255, 0.22);
-$border-h: rgba(30, 144, 255, 0.65);
-
-// 阴影
-$shadow: 0 4px 24px rgba(0, 80, 220, 0.22);
-
-// 字体
-$font-main: 'PingFang SC', 'Microsoft YaHei', 'Noto Sans SC', sans-serif;
-
-// 尺寸
-$screen-width: 9600px;
-$screen-height: 2800px;

+ 0 - 33
src/utils/request.js

@@ -1,33 +0,0 @@
-import axios from 'axios'
-
-// 创建 axios 实例
-const service = axios.create({
-  baseURL: process.env.VUE_APP_BASE_API || '/api',
-  timeout: 10000
-})
-
-// 请求拦截器
-service.interceptors.request.use(
-  config => {
-    const token = localStorage.getItem('token')
-    if (token) {
-      config.headers['Authorization'] = `Bearer ${token}`
-    }
-    return config
-  },
-  error => {
-    return Promise.reject(error)
-  }
-)
-
-// 响应拦截器
-service.interceptors.response.use(
-  response => {
-    return response.data
-  },
-  error => {
-    return Promise.reject(error)
-  }
-)
-
-export default service

+ 0 - 238
src/views/Login.vue

@@ -1,238 +0,0 @@
-<template>
-  <div class="login-container">
-    <div class="login-card">
-      <div class="login-header">
-        <h1>实验室安全智能监测与管控中心</h1>
-        <p>中国安全生产科学研究院</p>
-      </div>
-      <el-form
-        ref="loginForm"
-        :model="loginForm"
-        :rules="loginRules"
-        class="login-form"
-      >
-        <el-form-item prop="username">
-          <el-input
-            v-model="loginForm.username"
-            placeholder="请输入用户名/手机号"
-            prefix-icon="el-icon-user"
-          />
-        </el-form-item>
-        <el-form-item prop="password">
-          <el-input
-            v-model="loginForm.password"
-            type="password"
-            placeholder="请输入密码"
-            prefix-icon="el-icon-lock"
-            show-password
-            @keyup.enter.native="handleLogin"
-          />
-        </el-form-item>
-        <el-form-item prop="captcha">
-          <div class="captcha-row">
-            <el-input
-              v-model="loginForm.captcha"
-              placeholder="请输入验证码"
-              prefix-icon="el-icon-key"
-            />
-            <div class="captcha-img" @click="refreshCaptcha">
-              {{ captchaText }}
-            </div>
-          </div>
-        </el-form-item>
-        <el-form-item>
-          <el-button
-            type="primary"
-            :loading="loading"
-            class="login-btn"
-            @click="handleLogin"
-          >
-            登 录
-          </el-button>
-        </el-form-item>
-      </el-form>
-    </div>
-  </div>
-</template>
-
-<script>
-import md5 from 'js-md5'
-import { loginApi } from '@/api/auth'
-
-export default {
-  name: 'Login',
-  data() {
-    return {
-      loginForm: {
-        username: '',
-        password: '',
-        captcha: ''
-      },
-      loginRules: {
-        username: [
-          { required: true, message: '请输入用户名', trigger: 'blur' }
-        ],
-        password: [
-          { required: true, message: '请输入密码', trigger: 'blur' }
-        ],
-        captcha: [
-          { required: true, message: '请输入验证码', trigger: 'blur' }
-        ]
-      },
-      loading: false,
-      captchaText: '0000'
-    }
-  },
-  methods: {
-    refreshCaptcha() {
-      // 模拟验证码刷新,固定为 0000
-      this.captchaText = '0000'
-    },
-    handleLogin() {
-      this.$refs.loginForm.validate(async (valid) => {
-        if (!valid) return
-
-        // 验证码校验
-        if (this.loginForm.captcha !== '0000') {
-          this.$message.error('验证码错误')
-          return
-        }
-
-        this.loading = true
-        try {
-          // 密码使用 md5 加密后提交
-          const encryptedPassword = md5(this.loginForm.password)
-          const res = await loginApi({
-            username: this.loginForm.username,
-            password: encryptedPassword
-          })
-
-          if (res.code === 200) {
-            // 保存 token 到 Vuex
-            this.$store.dispatch('login', res.data.token)
-            this.$message.success('登录成功')
-            this.$router.push('/screen')
-          } else {
-            this.$message.error(res.message)
-          }
-        } catch (err) {
-          this.$message.error('登录失败,请重试')
-        } finally {
-          this.loading = false
-        }
-      })
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.login-container {
-  width: 100%;
-  height: 100%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  background: linear-gradient(135deg, #0a1a3a 0%, #0d2b5e 50%, #0a1a3a 100%);
-  position: relative;
-
-  &::before {
-    content: '';
-    position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    background:
-      radial-gradient(ellipse at 20% 50%, rgba(0, 100, 200, 0.15) 0%, transparent 50%),
-      radial-gradient(ellipse at 80% 50%, rgba(0, 150, 255, 0.1) 0%, transparent 50%);
-    pointer-events: none;
-  }
-}
-
-.login-card {
-  width: 480px;
-  padding: 48px 40px;
-  background: rgba(8, 30, 65, 0.9);
-  border: 1px solid rgba(46, 120, 210, 0.4);
-  border-radius: 12px;
-  backdrop-filter: blur(10px);
-  z-index: 1;
-}
-
-.login-header {
-  text-align: center;
-  margin-bottom: 40px;
-
-  h1 {
-    font-size: 24px;
-    color: $cyan;
-    margin-bottom: 8px;
-    letter-spacing: 2px;
-  }
-
-  p {
-    font-size: 14px;
-    color: $text-dim;
-  }
-}
-
-.login-form {
-  ::v-deep .el-input__inner {
-    background: rgba(6, 40, 90, 0.6);
-    border: 1px solid rgba(46, 120, 210, 0.3);
-    color: #fff;
-    height: 44px;
-
-    &::placeholder {
-      color: rgba(140, 180, 230, 0.5);
-    }
-
-    &:focus {
-      border-color: $cyan;
-    }
-  }
-
-  ::v-deep .el-input__prefix {
-    color: $cyan;
-  }
-}
-
-.captcha-row {
-  display: flex;
-  gap: 12px;
-
-  .el-input {
-    flex: 1;
-  }
-
-  .captcha-img {
-    width: 120px;
-    height: 44px;
-    background: rgba(6, 40, 90, 0.8);
-    border: 1px solid rgba(46, 120, 210, 0.3);
-    border-radius: 4px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    font-size: 20px;
-    color: $cyan;
-    letter-spacing: 8px;
-    cursor: pointer;
-    user-select: none;
-  }
-}
-
-.login-btn {
-  width: 100%;
-  height: 44px;
-  font-size: 16px;
-  background: linear-gradient(90deg, #1890ff, #00e5ff);
-  border: none;
-  letter-spacing: 8px;
-
-  &:hover {
-    opacity: 0.9;
-  }
-}
-</style>

+ 0 - 130
src/views/Screen.vue

@@ -1,130 +0,0 @@
-<template>
-  <div class="screen-container">
-    <div class="bg-hex"></div>
-    <div class="scan-effect"></div>
-    <ScreenHeader />
-    <div class="screen-content">
-      <!-- 1区:3个面板 -->
-      <div class="panel-col">
-        <EventStats />
-        <SafetyCompliance />
-        <PersonnelTrend />
-      </div>
-      <!-- 2区:2个面板 -->
-      <div class="panel-col">
-        <EnvMonitorStats />
-        <EquipmentStats />
-      </div>
-      <!-- 3区:实时监控 -->
-      <div class="center-col">
-        <SecurityMonitor />
-      </div>
-      <!-- 4区:环境感知 + 预警 -->
-      <div class="panel-col col-right">
-        <LabEnvironment />
-        <AlarmInfo />
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-import ScreenHeader from '@/components/ScreenHeader.vue'
-import EventStats from '@/components/EventStats.vue'
-import SafetyCompliance from '@/components/SafetyCompliance.vue'
-import PersonnelTrend from '@/components/PersonnelTrend.vue'
-import EnvMonitorStats from '@/components/EnvMonitorStats.vue'
-import EquipmentStats from '@/components/EquipmentStats.vue'
-import SecurityMonitor from '@/components/SecurityMonitor.vue'
-import LabEnvironment from '@/components/LabEnvironment.vue'
-import AlarmInfo from '@/components/AlarmInfo.vue'
-
-export default {
-  name: 'Screen',
-  components: {
-    ScreenHeader,
-    EventStats,
-    SafetyCompliance,
-    PersonnelTrend,
-    EnvMonitorStats,
-    EquipmentStats,
-    SecurityMonitor,
-    LabEnvironment,
-    AlarmInfo
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.screen-container {
-  width: 9600px;
-  height: 2800px;
-  position: relative;
-  overflow: hidden;
-  display: flex;
-  flex-direction: column;
-  background: $bg-deep;
-}
-
-/* 六角形背景纹理 */
-.bg-hex {
-  position: absolute;
-  inset: 0;
-  pointer-events: none;
-  z-index: 0;
-  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='52' viewBox='0 0 60 52'%3E%3Cpolygon points='30,2 58,17 58,47 30,52 2,47 2,17' fill='none' stroke='rgba(30,144,255,0.05)' stroke-width='1'/%3E%3C/svg%3E");
-  background-size: 150px 130px;
-
-  &::after {
-    content: '';
-    position: absolute;
-    inset: 0;
-    background: radial-gradient(ellipse at 50% 50%, transparent 25%, #030e1f 85%);
-  }
-}
-
-/* 主内容区域:4列精确像素布局 */
-.screen-content {
-  flex: 1;
-  min-height: 0;
-  display: grid;
-  grid-template-columns: 1892px 1414px 4282px 1892px;
-  column-gap: 20px;
-  padding: 20px 30px;
-  position: relative;
-  z-index: 2;
-}
-
-.panel-col {
-  display: flex;
-  flex-direction: column;
-  gap: 18px;
-  min-height: 0;
-}
-
-.center-col {
-  display: flex;
-  flex-direction: column;
-  min-height: 0;
-  position: relative;
-}
-
-/* 第1列:基本情况(956) + 安全分级(1017) + 人数走势(591) */
-.panel-col:nth-child(1) {
-  > :nth-child(1) { flex: 956; min-height: 0; }
-  > :nth-child(2) { flex: 1017; min-height: 0; }
-  > :nth-child(3) { flex: 591; min-height: 0; }
-}
-
-/* 第2列:设备统计(666) + 设备分类(1916) */
-.panel-col:nth-child(2) {
-  > :nth-child(1) { flex: 666; min-height: 0; }
-  > :nth-child(2) { flex: 1916; min-height: 0; }
-}
-
-/* 第4列:环境感知(1807) + 风险预警(775) */
-.col-right {
-  > :first-child { flex: 1807; min-height: 0; }
-  > :last-child { flex: 775; min-height: 0; }
-}
-</style>

+ 0 - 17
vue.config.js

@@ -1,17 +0,0 @@
-const { defineConfig } = require('@vue/cli-service')
-
-module.exports = defineConfig({
-  transpileDependencies: true,
-  lintOnSave: false,
-  devServer: {
-    port: 8080,
-    open: true
-  },
-  css: {
-    loaderOptions: {
-      sass: {
-        additionalData: `@import "@/styles/variables.scss";`
-      }
-    }
-  }
-})