dedsudiyu 1 hafta önce
ebeveyn
işleme
91bcc72eb6

+ 1 - 1
.env.development

@@ -5,7 +5,7 @@ NODE_ENV=development
 VUE_APP_TITLE=中国安全生产科学研究院实验室安全智慧化管控中心
 
 # 接口基础地址
-VUE_APP_BASE_API=http://localhost:9090/api
+VUE_APP_BASE_API=http://192.168.1.8/api
 
 # 接口超时时间(毫秒)
 VUE_APP_TIMEOUT=10000

+ 12 - 0
.idea/anKeYuan1080.iml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="WEB_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$">
+      <excludeFolder url="file://$MODULE_DIR$/.tmp" />
+      <excludeFolder url="file://$MODULE_DIR$/temp" />
+      <excludeFolder url="file://$MODULE_DIR$/tmp" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 6 - 0
.idea/misc.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="JavaScriptSettings">
+    <option name="languageLevel" value="ES6" />
+  </component>
+</project>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/anKeYuan1080.iml" filepath="$PROJECT_DIR$/.idea/anKeYuan1080.iml" />
+    </modules>
+  </component>
+</project>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>

+ 167 - 0
.idea/workspace.xml

@@ -0,0 +1,167 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ChangeListManager">
+    <list default="true" id="819ecefb-6243-4a26-9d25-b85c9c1e5444" name="Default Changelist" comment="">
+      <change beforePath="$PROJECT_DIR$/.env.development" beforeDir="false" afterPath="$PROJECT_DIR$/.env.development" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/src/api/index.js" beforeDir="false" afterPath="$PROJECT_DIR$/src/api/index.js" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/src/components/Header.vue" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/Header.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/src/components/LabStats/BasicStats.vue" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/LabStats/BasicStats.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/src/components/LabStats/DeviceStats.vue" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/LabStats/DeviceStats.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/src/components/LabStats/EnvSensing.vue" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/LabStats/EnvSensing.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/src/components/LabStats/EquipmentStats.vue" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/LabStats/EquipmentStats.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/src/components/LabStats/PersonStats.vue" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/LabStats/PersonStats.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/src/components/LabStats/RiskWarning.vue" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/LabStats/RiskWarning.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/src/components/LabStats/SafetyLevel.vue" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/LabStats/SafetyLevel.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/src/components/VideoMonitor/BuildingNav.vue" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/VideoMonitor/BuildingNav.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/src/components/VideoMonitor/VideoGrid.vue" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/VideoMonitor/VideoGrid.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/src/utils/request.js" beforeDir="false" afterPath="$PROJECT_DIR$/src/utils/request.js" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/src/views/Login.vue" beforeDir="false" afterPath="$PROJECT_DIR$/src/views/Login.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/vue.config.js" beforeDir="false" afterPath="$PROJECT_DIR$/vue.config.js" afterDir="false" />
+    </list>
+    <ignored path="$PROJECT_DIR$/dist/" />
+    <ignored path="$PROJECT_DIR$/.tmp/" />
+    <ignored path="$PROJECT_DIR$/temp/" />
+    <ignored path="$PROJECT_DIR$/tmp/" />
+    <option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" />
+    <option name="SHOW_DIALOG" value="false" />
+    <option name="HIGHLIGHT_CONFLICTS" value="true" />
+    <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
+    <option name="LAST_RESOLUTION" value="IGNORE" />
+  </component>
+  <component name="FUSProjectUsageTrigger">
+    <session id="-1735258732">
+      <usages-collector id="statistics.lifecycle.project">
+        <counts>
+          <entry key="project.closed" value="2" />
+          <entry key="project.open.time.1" value="1" />
+          <entry key="project.open.time.3" value="1" />
+          <entry key="project.opened" value="2" />
+        </counts>
+      </usages-collector>
+    </session>
+  </component>
+  <component name="FindInProjectRecents">
+    <findStrings>
+      <find>getTriggerInfo</find>
+      <find>mqtt</find>
+    </findStrings>
+    <dirStrings>
+      <dir>E:\git\2021项目\anKeYuan1080\src</dir>
+    </dirStrings>
+  </component>
+  <component name="Git.Settings">
+    <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
+  </component>
+  <component name="JsBuildToolGruntFileManager" detection-done="true" sorting="DEFINITION_ORDER" />
+  <component name="JsBuildToolPackageJson" detection-done="true" sorting="DEFINITION_ORDER">
+    <package-json value="$PROJECT_DIR$/package.json" />
+  </component>
+  <component name="JsGulpfileManager">
+    <detection-done>true</detection-done>
+    <sorting>DEFINITION_ORDER</sorting>
+  </component>
+  <component name="NodeModulesDirectoryManager">
+    <handled-path value="$PROJECT_DIR$/node_modules" />
+  </component>
+  <component name="NodePackageJsonFileManager">
+    <packageJsonPaths>
+      <path value="$PROJECT_DIR$/package.json" />
+    </packageJsonPaths>
+  </component>
+  <component name="ProjectFrameBounds" extendedState="6">
+    <option name="x" value="-8" />
+    <option name="y" value="-8" />
+    <option name="width" value="1936" />
+    <option name="height" value="1056" />
+  </component>
+  <component name="ProjectView">
+    <navigator proportions="" version="1">
+      <foldersAlwaysOnTop value="true" />
+    </navigator>
+    <panes>
+      <pane id="Scope" />
+      <pane id="ProjectPane">
+        <subPane>
+          <expand>
+            <path>
+              <item name="anKeYuan1080" type="b2602c69:ProjectViewProjectNode" />
+              <item name="anKeYuan1080" type="462c0819:PsiDirectoryNode" />
+            </path>
+            <path>
+              <item name="anKeYuan1080" type="b2602c69:ProjectViewProjectNode" />
+              <item name="anKeYuan1080" type="462c0819:PsiDirectoryNode" />
+              <item name="src" type="462c0819:PsiDirectoryNode" />
+            </path>
+          </expand>
+          <select />
+        </subPane>
+      </pane>
+    </panes>
+  </component>
+  <component name="PropertiesComponent">
+    <property name="WebServerToolWindowFactoryState" value="false" />
+    <property name="last_opened_file_path" value="$PROJECT_DIR$/../v3-PC端" />
+    <property name="nodejs_interpreter_path.stuck_in_default_project" value="undefined stuck path" />
+    <property name="nodejs_npm_path_reset_for_default_project" value="true" />
+    <property name="prettierjs.PrettierConfiguration.Package" value="E:\git\2021项目\anKeYuan1080\node_modules\prettier" />
+  </component>
+  <component name="RunDashboard">
+    <option name="ruleStates">
+      <list>
+        <RuleState>
+          <option name="name" value="ConfigurationTypeDashboardGroupingRule" />
+        </RuleState>
+        <RuleState>
+          <option name="name" value="StatusDashboardGroupingRule" />
+        </RuleState>
+      </list>
+    </option>
+  </component>
+  <component name="SvnConfiguration">
+    <configuration />
+  </component>
+  <component name="TaskManager">
+    <task active="true" id="Default" summary="Default task">
+      <changelist id="819ecefb-6243-4a26-9d25-b85c9c1e5444" name="Default Changelist" comment="" />
+      <created>1773912834911</created>
+      <option name="number" value="Default" />
+      <option name="presentableId" value="Default" />
+      <updated>1773912834911</updated>
+      <workItem from="1773912836836" duration="662000" />
+      <workItem from="1773914515310" duration="92000" />
+    </task>
+    <servers />
+  </component>
+  <component name="TimeTrackingManager">
+    <option name="totallyTimeSpent" value="754000" />
+  </component>
+  <component name="ToolWindowManager">
+    <frame x="-8" y="-8" width="1936" height="1056" extended-state="6" />
+    <layout>
+      <window_info active="true" content_ui="combo" id="Project" order="0" visible="true" weight="0.24973656" />
+      <window_info id="Structure" order="1" side_tool="true" weight="0.25" />
+      <window_info id="npm" order="2" side_tool="true" />
+      <window_info id="Favorites" order="3" side_tool="true" />
+      <window_info anchor="bottom" id="Message" order="0" />
+      <window_info anchor="bottom" id="Find" order="1" />
+      <window_info anchor="bottom" id="Run" order="2" />
+      <window_info anchor="bottom" id="Debug" order="3" weight="0.4" />
+      <window_info anchor="bottom" id="Cvs" order="4" weight="0.25" />
+      <window_info anchor="bottom" id="Inspection" order="5" weight="0.4" />
+      <window_info anchor="bottom" id="TODO" order="6" />
+      <window_info anchor="bottom" id="Docker" order="7" show_stripe_button="false" />
+      <window_info anchor="bottom" id="Version Control" order="8" />
+      <window_info anchor="bottom" id="Event Log" order="9" side_tool="true" />
+      <window_info anchor="bottom" id="Terminal" order="10" />
+      <window_info anchor="right" id="Commander" internal_type="SLIDING" order="0" type="SLIDING" weight="0.4" />
+      <window_info anchor="right" id="Ant Build" order="1" weight="0.25" />
+      <window_info anchor="right" content_ui="combo" id="Hierarchy" order="2" weight="0.25" />
+    </layout>
+  </component>
+  <component name="TypeScriptGeneratedFilesManager">
+    <option name="version" value="1" />
+  </component>
+  <component name="VcsContentAnnotationSettings">
+    <option name="myLimit" value="2678400000" />
+  </component>
+</project>

+ 2 - 0
.vscode/settings.json

@@ -0,0 +1,2 @@
+{
+}

+ 175 - 225
src/api/index.js

@@ -1,255 +1,205 @@
 /**
- * 模拟API接口
- * 所有接口使用setTimeout模拟网络请求
+ * API 接口
+ * 基础路径: /laboratory/labScreen (lab), /auth (auth)
  */
+import request from '@/utils/request'
 
-// 模拟延迟
-function mockDelay(data, delay = 500) {
-  return new Promise(resolve => {
-    setTimeout(() => resolve({ code: 200, data, msg: 'success' }), delay)
-  })
+function wrapResult(data) {
+  return { code: 200, data, msg: 'success' }
+}
+
+const PIE_COLORS = ['#48d7ff', '#3a7bff', '#36d399', '#ffb020', '#ff8c00', '#ff4d4f', '#a78bfa', '#e91e63', '#00e5ff', '#9c27b0']
+
+// ======================== 1. 实验室基本情况统计 ========================
+let _labBasicCache = null
+
+async function fetchLabBasic() {
+  if (_labBasicCache) return _labBasicCache
+  const res = await request.get('/laboratory/labScreen/labBasicStatistics')
+  if (res.code === 200 && res.data) { _labBasicCache = res.data; return res.data }
+  throw new Error(res.message || '接口异常')
 }
 
-// 登录接口
-export function loginApi(params) {
-  return mockDelay({
-    token: 'mock-token-' + Date.now(),
-    userInfo: {
-      name: '管理员',
-      role: 'admin'
-    }
-  }, 800)
+function clearLabBasicCache() {
+  setTimeout(() => { _labBasicCache = null }, 5000)
 }
 
-// 实验室基本情况统计
-export function getLabBasicStats() {
-  return mockDelay({
-    totalLabs: 58,
+export async function getLabBasicStats() {
+  const raw = await fetchLabBasic()
+  clearLabBasicCache()
+  return wrapResult({
+    totalLabs: raw.labTotal,
     levels: [
-      { name: 'I级(红)', value: 12, color: '#f44336' },
-      { name: 'II级(橙)', value: 18, color: '#ff9800' },
-      { name: 'III级(黄)', value: 16, color: '#ffc107' },
-      { name: 'IV级(蓝)', value: 12, color: '#2196f3' }
+      { name: 'I级(红)', value: raw.levelOneCount, color: '#f44336' },
+      { name: 'II级(橙)', value: raw.levelTwoCount, color: '#ff9800' },
+      { name: 'III级(黄)', value: raw.levelThreeCount, color: '#ffc107' },
+      { name: 'IV级(蓝)', value: raw.levelFourCount, color: '#2196f3' }
     ],
-    usage: {
-      using: 20,
-      abnormal: 3,
-      idle: 35
-    }
+    usage: { using: raw.useTotal, abnormal: raw.exceptionalTotal, idle: raw.availableTotal }
   })
 }
 
-// 实验室安全分级统计
-export function getSafetyLevelStats() {
-  return mockDelay({
-    departments: [
-      { name: '安全技术\n研究所', total: 11, level1: 3, level2: 3, level3: 3, level4: 2 },
-      { name: '职业安全\n研究所', total: 9, level1: 2, level2: 3, level3: 2, level4: 2 },
-      { name: '化学品安全\n研究所', total: 12, level1: 3, level2: 4, level3: 3, level4: 2 },
-      { name: '矿山安全\n研究所', total: 10, level1: 2, level2: 3, level3: 3, level4: 2 },
-      { name: '应急科学\n研究中心', total: 14, level1: 4, level2: 4, level3: 3, level4: 3 },
-      { name: '信息技术\n研究所', total: 8, level1: 2, level2: 2, level3: 2, level4: 2 },
-      { name: '检测检验\n中心', total: 6, level1: 1, level2: 2, level3: 2, level4: 1 },
-      { name: '标准化\n研究所', total: 4, level1: 1, level2: 1, level3: 1, level4: 1 }
-    ]
-  })
+// ======================== 2. 实验室安全分级统计(复用 labBasicStatistics 数据)========================
+export async function getSafetyLevelStats() {
+  const raw = await fetchLabBasic()
+  clearLabBasicCache()
+  const departments = (raw.labLevelVOList || []).map(d => ({
+    name: d.deptName,
+    total: (d.levelOneCount || 0) + (d.levelTwoCount || 0) + (d.levelThreeCount || 0) + (d.levelFourCount || 0),
+    level1: d.levelOneCount || 0,
+    level2: d.levelTwoCount || 0,
+    level3: d.levelThreeCount || 0,
+    level4: d.levelFourCount || 0
+  }))
+  return wrapResult({ departments })
 }
 
-// 实验室进入人数统计及走势
-export function getPersonStats() {
-  return mockDelay({
-    todayTotal: [0, 1, 2, 8, 6],
-    currentLab: [0, 0, 4, 7],
-    hourlyData: [
-      { hour: '0:00', enter: 5, lab: 2 },
-      { hour: '3:00', enter: 3, lab: 1 },
-      { hour: '6:00', enter: 20, lab: 8 },
-      { hour: '9:00', enter: 135, lab: 85 },
-      { hour: '12:00', enter: 98, lab: 60 },
-      { hour: '15:00', enter: 110, lab: 72 },
-      { hour: '18:00', enter: 45, lab: 25 },
-      { hour: '21:00', enter: 15, lab: 5 },
-      { hour: '24:00', enter: 3, lab: 1 }
-    ]
-  })
+// ======================== 3. 实验室进入人数统计及走势 ========================
+export async function getPersonStats() {
+  const res = await request.get('/laboratory/labScreen/passOutTrend')
+  if (res.code === 200 && res.data) {
+    const d = res.data
+    const toDigits = n => String(n).split('').map(Number)
+    const hourMap = {}
+    ;(d.entryTrend || []).forEach(t => { hourMap[t.hour] = { enter: t.count, lab: 0 } })
+    ;(d.labUserTrend || []).forEach(t => {
+      if (!hourMap[t.hour]) hourMap[t.hour] = { enter: 0, lab: 0 }
+      hourMap[t.hour].lab = t.count
+    })
+    const hourlyData = Object.keys(hourMap).sort((a, b) => a - b).map(h => ({
+      hour: h + ':00', enter: hourMap[h].enter, lab: hourMap[h].lab
+    }))
+    return wrapResult({ todayTotal: toDigits(d.todayEntryCount || 0), currentLab: toDigits(d.currentLabUserCount || 0), hourlyData })
+  }
+  throw new Error(res.message || '接口异常')
 }
 
-// 实验室环境安全智能感知
-export function getEnvSensingList() {
-  return mockDelay({
-    list: [
-      { id: 1, lab: '有机化学实验室A103(A103)', dept: '化学品安全研究所', temp: 23.4, humidity: 37, tvoc: 0.83, co2: { value: 1071, warn: true }, o2: { value: 20.4, warn: false } },
-      { id: 2, lab: '无损检测实验室B205(B205)', dept: '安全技术研究所', temp: 25.9, humidity: 44, tvoc: 0.96, co2: { value: 452, warn: false }, o2: { value: 20.1, warn: false } },
-      { id: 3, lab: '化学实验室A101(A101)', dept: '化学品安全研究所', temp: 23.4, humidity: 43, tvoc: 0.72, co2: { value: 923, warn: true }, o2: { value: 19.9, warn: true } },
-      { id: 4, lab: '材料力学实验室B203(B203)', dept: '安全技术研究所', temp: 25.8, humidity: 77, tvoc: 0.69, co2: { value: 862, warn: false }, o2: { value: 18.5, warn: true } },
-      { id: 5, lab: '气体分析实验室A305(A305)', dept: '职业安全研究所', temp: 18.2, humidity: 73, tvoc: 0.11, co2: { value: 396, warn: false }, o2: { value: 21.2, warn: false } },
-      { id: 6, lab: '高温高压实验室C102(C102)', dept: '矿山安全研究所', temp: 26.3, humidity: 31, tvoc: 1.74, co2: { value: 947, warn: false }, o2: { value: 20.6, warn: false } },
-      { id: 7, lab: '生物安全实验室A202(A202)', dept: '职业安全研究所', temp: 21.3, humidity: 32, tvoc: 1.69, co2: { value: 444, warn: false }, o2: { value: 20.7, warn: false } },
-      { id: 8, lab: '粉尘检测实验室B101(B101)', dept: '矿山安全研究所', temp: 28.8, humidity: 78, tvoc: 0.79, co2: { value: 871, warn: false }, o2: { value: 20.7, warn: false } },
-      { id: 9, lab: '电气安全实验室C201(C201)', dept: '安全技术研究所', temp: 22.6, humidity: 41, tvoc: 0.55, co2: { value: 523, warn: false }, o2: { value: 20.9, warn: false } },
-      { id: 10, lab: '防爆测试实验室C301(C301)', dept: '矿山安全研究所', temp: 24.1, humidity: 39, tvoc: 0.88, co2: { value: 612, warn: false }, o2: { value: 20.5, warn: false } },
-      { id: 11, lab: '热分析实验室A401(A401)', dept: '化学品安全研究所', temp: 27.2, humidity: 45, tvoc: 1.12, co2: { value: 789, warn: false }, o2: { value: 20.3, warn: false } }
-    ]
-  })
+// ======================== 4. 实验环境安全智能感知 ========================
+export async function getEnvSensingList() {
+  const res = await request.get('/laboratory/labScreen/envMonitor', { params: { page: 1, pageSize: 50 } })
+  if (res.code === 200 && res.data && res.data.records) {
+    const list = res.data.records.map((r, i) => {
+      const sensors = r.sensorList || []
+      const find = code => sensors.find(s => s.code === code)
+      const tempS = find('temperature'), humS = find('humidity'), tvocS = find('tvoc'), co2S = find('co2'), o2S = find('o2')
+      return {
+        id: r.subId || i + 1,
+        lab: r.subName + (r.roomNum ? `(${r.roomNum})` : ''),
+        dept: '',
+        temp: tempS ? tempS.deviceValue : '-',
+        humidity: humS ? humS.deviceValue : '-',
+        tvoc: tvocS ? tvocS.deviceValue : '-',
+        co2: { value: co2S ? co2S.deviceValue : '-', warn: co2S ? !co2S.operatingState : false },
+        o2: { value: o2S ? o2S.deviceValue : '-', warn: o2S ? !o2S.operatingState : false }
+      }
+    })
+    return wrapResult({ list })
+  }
+  throw new Error(res.message || '接口异常')
 }
 
-// 实验室实时风险预警
-export function getRiskWarningList() {
-  return mockDelay({
-    totalMonth: 12,
-    list: [
-      { id: 1, lab: '高温高压实验室C102(C102)', dept: '矿山安全研究所', type: '温度', value: '35.6°C', time: '2026-03-04 14:28:05', state: 'pending' },
-      { id: 2, lab: '气体分析实验室A305(A305)', dept: '职业安全研究所', type: 'CO₂', value: '1050ppm', time: '2026-03-04 13:55:42', state: 'processing' },
-      { id: 3, lab: '气体分析实验室A304(A304)', dept: '职业安全研究所', type: 'CO₂', value: '1051ppm', time: '2026-03-04 13:55:41', state: 'pending' },
-      { id: 4, lab: '粉尘检测实验室B101(B101)', dept: '矿山安全研究所', type: 'O₂', value: '18.2%', time: '2026-03-04 13:20:10', state: 'processing' },
-      { id: 5, lab: '有机化学实验室A103(A103)', dept: '化学品安全研究所', type: 'TVOC', value: '2.1mg/m³', time: '2026-03-04 12:45:30', state: 'resolved' },
-      { id: 6, lab: '电气安全实验室C201(C201)', dept: '安全技术研究所', type: '湿度', value: '85%', time: '2026-03-04 11:30:15', state: 'resolved' }
-    ]
-  })
+// ======================== 5. 实验室实时风险预警 ========================
+export async function getRiskWarningList() {
+  const res = await request.get('/laboratory/labScreen/riskWarning')
+  if (res.code === 200 && res.data) {
+    const d = res.data
+    return wrapResult({
+      totalMonth: d.monthAlarmCount,
+      list: (d.eventList || []).map(e => ({
+        id: e.id, lab: e.subName, dept: '', type: e.eventName, value: '',
+        time: e.eventStartTime ? e.eventStartTime.replace('T', ' ') : ''
+      }))
+    })
+  }
+  throw new Error(res.message || '接口异常')
 }
 
-// 智能环境感知应用设备统计
-export function getDeviceStats() {
-  return mockDelay({
-    online: 186,
-    offline: 14,
-    onlineRate: 93,
-    devices: [
-      { name: '电子信息铭牌', count: 58 },
-      { name: '化学品智能终端', count: 32 },
-      { name: '传感器', count: 76 },
-      { name: '智能设备', count: 34 }
-    ]
-  })
+// ======================== 6. 智能环境感知应用设备统计 ========================
+export async function getDeviceStats() {
+  const res = await request.get('/laboratory/labScreen/iotDeviceStatistics')
+  if (res.code === 200 && res.data) {
+    const d = res.data
+    return wrapResult({
+      online: d.onlineCount, offline: d.offlineCount, onlineRate: d.onlineRate,
+      devices: [
+        { name: '电子信息铭牌', count: d.tagCount || 0 },
+        { name: '化学品智能终端', count: d.terminalCount || 0 },
+        { name: '传感器', count: d.sensorCount || 0 },
+        { name: '摄像头', count: d.cameraCount || 0 },
+        { name: '智能设备', count: d.iotCount || 0 }
+      ].filter(i => i.count > 0)
+    })
+  }
+  throw new Error(res.message || '接口异常')
 }
 
-// 实验室设备分类及使用统计
-export function getEquipmentStats() {
-  return mockDelay({
-    categories: [
-      { name: '分析仪器', value: 120, color: '#48d7ff' },
-      { name: '安全防护', value: 95, color: '#3a7bff' },
-      { name: '化学试剂设备', value: 80, color: '#36d399' },
-      { name: '电气设备', value: 75, color: '#ffb020' },
-      { name: '力学测试', value: 86, color: '#ff8c00' },
-      { name: '环境监测', value: 72, color: '#ff4d4f' },
-      { name: '通用设备', value: 58, color: '#a78bfa' }
-    ],
-    summary: {
-      totalDevices: 586,
-      totalHours: '12,480',
-      usageRate: '78.6'
-    },
-    status: {
-      using: 128,
-      idle: 312,
-      normal: 108,
-      repair: 38
-    }
-  })
+// ======================== 7. 实验室设备分类及使用统计 ========================
+export async function getEquipmentStats() {
+  const res = await request.get('/laboratory/labScreen/deviceCategoryStat')
+  if (res.code === 200 && res.data) {
+    const d = res.data
+    return wrapResult({
+      categories: (d.categoryList || []).map((c, i) => ({
+        name: c.categoryOneName, value: c.totalCount, color: PIE_COLORS[i % PIE_COLORS.length]
+      })),
+      summary: {
+        totalDevices: d.totalCount,
+        totalHours: d.totalUsageHours ? d.totalUsageHours.toLocaleString() : '0',
+        usageRate: String(d.usageRate || 0)
+      },
+      status: { using: d.inUseCount, idle: d.idleCount, normal: d.normalCount, repair: d.repairCount }
+    })
+  }
+  throw new Error(res.message || '接口异常')
 }
 
-// 天气接口(后续对接真实后台)
-export function getWeather() {
-  return mockDelay({
-    city: '北京',
-    weather: '晴',
-    temp: 18
-  })
+// ======================== 天气接口 ========================
+export async function getWeather() {
+  const res = await request.get('/laboratory/labScreen/weather')
+  if (res.code === 200 && res.data) return wrapResult(res.data)
+  throw new Error(res.message || '接口异常')
 }
 
-// 建筑结构导航(视频监控页)
-export function getBuildingTree() {
-  return mockDelay({
-    tree: [
-      {
-        id: 1,
-        label: '安科院院区',
-        children: [
-          {
-            id: 11,
-            label: '科研楼A',
-            children: [
-              { id: 111, label: '1层', children: [
-                { id: 1111, label: 'A101 化学实验室' },
-                { id: 1112, label: 'A102 有机合成实验室' },
-                { id: 1113, label: 'A103 有机化学实验室' }
-              ]},
-              { id: 112, label: '2层', children: [
-                { id: 1121, label: 'A201 材料分析实验室' },
-                { id: 1122, label: 'A202 生物安全实验室' }
-              ]},
-              { id: 113, label: '3层', children: [
-                { id: 1131, label: 'A301 气体分析实验室' },
-                { id: 1132, label: 'A305 气体检测实验室' }
-              ]},
-              { id: 114, label: '4层', children: [
-                { id: 1141, label: 'A401 热分析实验室' }
-              ]}
-            ]
-          },
-          {
-            id: 12,
-            label: '科研楼B',
-            children: [
-              { id: 121, label: '1层', children: [
-                { id: 1211, label: 'B101 粉尘检测实验室' }
-              ]},
-              { id: 122, label: '2层', children: [
-                { id: 1221, label: 'B203 材料力学实验室' },
-                { id: 1222, label: 'B205 无损检测实验室' }
-              ]}
-            ]
-          },
-          {
-            id: 13,
-            label: '科研楼C',
-            children: [
-              { id: 131, label: '1层', children: [
-                { id: 1311, label: 'C102 高温高压实验室' }
-              ]},
-              { id: 132, label: '2层', children: [
-                { id: 1321, label: 'C201 电气安全实验室' }
-              ]},
-              { id: 133, label: '3层', children: [
-                { id: 1331, label: 'C301 防爆测试实验室' }
-              ]}
-            ]
-          }
-        ]
-      }
-    ]
-  })
+// ======================== 8. 建筑结构导航 ========================
+function transformTree(nodes) {
+  if (!nodes) return []
+  return nodes.map(n => ({ id: n.id, label: n.name, children: transformTree(n.buildFloorVoList) }))
 }
 
-// 视频列表
-export function getVideoList(params = {}) {
-  const allVideos = [
-    { id: 1, name: 'A101 化学实验室', ai: false, status: 'online' },
-    { id: 2, name: 'A103 有机化学实验室', ai: true, status: 'online' },
-    { id: 3, name: 'A202 生物安全实验室', ai: false, status: 'online' },
-    { id: 4, name: 'A305 气体检测实验室', ai: false, status: 'online' },
-    { id: 5, name: 'A401 热分析实验室', ai: true, status: 'online' },
-    { id: 6, name: 'B101 粉尘检测实验室', ai: false, status: 'online' },
-    { id: 7, name: 'B203 材料力学实验室', ai: false, status: 'online' },
-    { id: 8, name: 'B205 无损检测实验室', ai: false, status: 'offline' },
-    { id: 9, name: 'C102 高温高压实验室', ai: true, status: 'online' },
-    { id: 10, name: 'C201 电气安全实验室', ai: false, status: 'online' },
-    { id: 11, name: 'C301 防爆测试实验室', ai: false, status: 'online' },
-    { id: 12, name: 'A102 有机合成实验室', ai: false, status: 'online' },
-    { id: 13, name: 'A201 材料分析实验室', ai: false, status: 'offline' },
-    { id: 14, name: 'A301 气体分析实验室', ai: true, status: 'online' },
-    { id: 15, name: '园区东门', ai: false, status: 'online' },
-    { id: 16, name: '园区西门', ai: false, status: 'online' },
-    { id: 17, name: '停车场', ai: false, status: 'online' },
-    { id: 18, name: '走廊通道A3F', ai: false, status: 'online' }
-  ]
-  const page = params.page || 1
-  const start = (page - 1) * 9
-  const list = allVideos.slice(start, start + 9)
-  return mockDelay({
-    total: allVideos.length,
-    page,
-    list
+export async function getBuildingTree(params = {}) {
+  const res = await request.post('/laboratory/labScreen/monitorTree', params)
+  if (res.code === 200 && res.data) return wrapResult({ tree: transformTree(res.data) })
+  throw new Error(res.message || '接口异常')
+}
+
+// ======================== 9. 视频列表 ========================
+export async function getVideoList(params = {}) {
+  const res = await request.post('/laboratory/labScreen/cameraStream', {
+    subIds: params.subIds || [], streamType: 0, protocol: 'ws', source: 3,
+    page: params.page || 1, pageSize: params.pageSize || 9
   })
+  if (res.code === 200 && res.data && res.data.records) {
+    return wrapResult({
+      total: res.data.total, page: res.data.current,
+      list: res.data.records.map(r => ({
+        id: r.deviceNo, name: r.subjectName + (r.room ? `(${r.room})` : ''),
+        ai: false, status: r.streamUrl ? 'online' : 'offline', streamUrl: r.streamUrl
+      }))
+    })
+  }
+  throw new Error(res.message || '接口异常')
+}
+
+// ======================== 登录接口 ========================
+export async function loginApi(params) {
+  const res = await request.post('/auth/bigLogin', params)
+  if (res.code === 200 && res.data) return wrapResult(res.data)
+  throw new Error(res.message || res.msg || '登录失败')
+}
+
+// ======================== 7. 风险预警-触发预案信息 ========================
+export async function getTriggerInfo() {
+  const res = await request.get('/laboratory/large/selectTriggerInfo')
+  if (res.code === 200 && res.data) return wrapResult(res.data)
+  throw new Error(res.message || '接口异常')
 }

+ 1 - 1
src/components/Header.vue

@@ -64,7 +64,7 @@ export default {
       try {
         const res = await getWeather()
         const d = res.data
-        this.weather = `☁ ${d.city} · ${d.weather} ${d.temp}°C`
+        this.weather = `☁ ${d.weatherDesc} ${d.temperature}°C`
       } catch (e) {
         this.weather = '☁ 北京 · 晴 18°C'
       }

+ 21 - 7
src/components/LabStats/BasicStats.vue

@@ -33,19 +33,33 @@ export default {
     return {
       levels: [],
       usage: { using: 0, abnormal: 0, idle: 0 },
-      chart: null
+      chart: null,
+      timer: null
     }
   },
   async mounted() {
-    const res = await getLabBasicStats()
-    this.levels = res.data.levels
-    this.usage = res.data.usage
-    this.$nextTick(() => this.initChart(res.data))
+    await this.loadData()
+    this.timer = setInterval(this.loadData, 5 * 60 * 1000)
   },
   beforeDestroy() {
     if (this.chart) this.chart.dispose()
+    clearInterval(this.timer)
   },
   methods: {
+    async loadData() {
+      try {
+        const res = await getLabBasicStats()
+        this.levels = res.data.levels
+        this.usage = res.data.usage
+        if (this.chart) {
+          this.chart.setOption({ series: [{ label: { formatter: res.data.totalLabs + '\n实验室总数' }, data: res.data.levels.map(l => ({ name: l.name, value: l.value, itemStyle: { color: l.color } })) }] })
+        } else {
+          this.$nextTick(() => this.initChart(res.data))
+        }
+      } catch (e) {
+        console.error('BasicStats:', e)
+      }
+    },
     initChart(data) {
       this.chart = echarts.init(this.$refs.chart)
       const option = {
@@ -65,8 +79,8 @@ export default {
         },
         series: [{
           type: 'pie',
-          radius: ['45%', '70%'],
-          center: ['35%', '50%'],
+          radius: ['60%', '85%'],
+          center: ['45%', '50%'],
           label: {
             show: true,
             position: 'center',

+ 19 - 6
src/components/LabStats/DeviceStats.vue

@@ -34,19 +34,32 @@ import { getDeviceStats } from '@/api'
 export default {
   name: 'DeviceStats',
   data() {
-    return { online: 0, offline: 0, devices: [], chart: null }
+    return { online: 0, offline: 0, devices: [], chart: null, timer: null }
   },
   async mounted() {
-    const res = await getDeviceStats()
-    this.online = res.data.online
-    this.offline = res.data.offline
-    this.devices = res.data.devices
-    this.$nextTick(() => this.initChart(res.data.onlineRate))
+    await this.loadData()
+    this.timer = setInterval(this.loadData, 5 * 60 * 1000)
   },
   beforeDestroy() {
     if (this.chart) this.chart.dispose()
+    clearInterval(this.timer)
   },
   methods: {
+    async loadData() {
+      try {
+        const res = await getDeviceStats()
+        this.online = res.data.online
+        this.offline = res.data.offline
+        this.devices = res.data.devices
+        if (this.chart) {
+          this.chart.setOption({ series: [{ data: [{ value: res.data.onlineRate, name: '设备在线率' }] }] })
+        } else {
+          this.$nextTick(() => this.initChart(res.data.onlineRate))
+        }
+      } catch (e) {
+        console.error('DeviceStats:', e)
+      }
+    },
     initChart(rate) {
       this.chart = echarts.init(this.$refs.chart)
       const option = {

+ 16 - 6
src/components/LabStats/EnvSensing.vue

@@ -36,7 +36,8 @@ export default {
       list: [],
       paused: false,
       scrollTimer: null,
-      scrollY: 0
+      scrollY: 0,
+      timer: null
     }
   },
   computed: {
@@ -46,16 +47,25 @@ export default {
     }
   },
   async mounted() {
-    const res = await getEnvSensingList()
-    this.list = res.data.list
-    this.$nextTick(() => {
-      setTimeout(() => this.startScroll(), 500)
-    })
+    await this.loadData()
+    this.timer = setInterval(this.loadData, 5 * 60 * 1000)
   },
   beforeDestroy() {
     if (this.scrollTimer) cancelAnimationFrame(this.scrollTimer)
+    clearInterval(this.timer)
   },
   methods: {
+    async loadData() {
+      try {
+        const res = await getEnvSensingList()
+        this.list = res.data.list
+        if (!this.scrollTimer) {
+          this.$nextTick(() => setTimeout(() => this.startScroll(), 500))
+        }
+      } catch (e) {
+        console.error('EnvSensing:', e)
+      }
+    },
     startScroll() {
       const wrap = this.$refs.scrollWrap
       const inner = this.$refs.scrollInner

+ 19 - 5
src/components/LabStats/EquipmentStats.vue

@@ -39,19 +39,33 @@ export default {
     return {
       chart: null,
       summary: { totalDevices: 0, totalHours: '', usageRate: '' },
-      status: { using: 0, idle: 0, normal: 0, repair: 0 }
+      status: { using: 0, idle: 0, normal: 0, repair: 0 },
+      timer: null
     }
   },
   async mounted() {
-    const res = await getEquipmentStats()
-    this.summary = res.data.summary
-    this.status = res.data.status
-    this.$nextTick(() => this.initChart(res.data.categories))
+    await this.loadData()
+    this.timer = setInterval(this.loadData, 5 * 60 * 1000)
   },
   beforeDestroy() {
     if (this.chart) this.chart.dispose()
+    clearInterval(this.timer)
   },
   methods: {
+    async loadData() {
+      try {
+        const res = await getEquipmentStats()
+        this.summary = res.data.summary
+        this.status = res.data.status
+        if (this.chart) {
+          this.chart.setOption({ series: [{ data: res.data.categories.map(c => ({ name: c.name, value: c.value, itemStyle: { color: c.color } })) }] })
+        } else {
+          this.$nextTick(() => this.initChart(res.data.categories))
+        }
+      } catch (e) {
+        console.error('EquipmentStats:', e)
+      }
+    },
     initChart(categories) {
       this.chart = echarts.init(this.$refs.chart)
       const option = {

+ 22 - 5
src/components/LabStats/PersonStats.vue

@@ -33,19 +33,36 @@ export default {
     return {
       todayTotal: [0, 0, 0, 0, 0],
       currentLab: [0, 0, 0, 0],
-      chart: null
+      chart: null,
+      timer: null
     }
   },
   async mounted() {
-    const res = await getPersonStats()
-    this.todayTotal = res.data.todayTotal
-    this.currentLab = res.data.currentLab
-    this.$nextTick(() => this.initChart(res.data.hourlyData))
+    await this.loadData()
+    this.timer = setInterval(this.loadData, 5 * 60 * 1000)
   },
   beforeDestroy() {
     if (this.chart) this.chart.dispose()
+    clearInterval(this.timer)
   },
   methods: {
+    async loadData() {
+      try {
+        const res = await getPersonStats()
+        this.todayTotal = res.data.todayTotal
+        this.currentLab = res.data.currentLab
+        if (this.chart) {
+          this.chart.setOption({
+            xAxis: { data: res.data.hourlyData.map(d => d.hour) },
+            series: [{ data: res.data.hourlyData.map(d => d.enter) }, { data: res.data.hourlyData.map(d => d.lab) }]
+          })
+        } else {
+          this.$nextTick(() => this.initChart(res.data.hourlyData))
+        }
+      } catch (e) {
+        console.error('PersonStats:', e)
+      }
+    },
     initChart(data) {
       this.chart = echarts.init(this.$refs.chart)
       const option = {

+ 17 - 7
src/components/LabStats/RiskWarning.vue

@@ -32,7 +32,8 @@ export default {
       list: [],
       paused: false,
       scrollTimer: null,
-      scrollY: 0
+      scrollY: 0,
+      timer: null
     }
   },
   computed: {
@@ -41,17 +42,26 @@ export default {
     }
   },
   async mounted() {
-    const res = await getRiskWarningList()
-    this.totalMonth = res.data.totalMonth
-    this.list = res.data.list
-    this.$nextTick(() => {
-      setTimeout(() => this.startScroll(), 500)
-    })
+    await this.loadData()
+    this.timer = setInterval(this.loadData, 5 * 60 * 1000)
   },
   beforeDestroy() {
     if (this.scrollTimer) cancelAnimationFrame(this.scrollTimer)
+    clearInterval(this.timer)
   },
   methods: {
+    async loadData() {
+      try {
+        const res = await getRiskWarningList()
+        this.totalMonth = res.data.totalMonth
+        this.list = res.data.list
+        if (!this.scrollTimer) {
+          this.$nextTick(() => setTimeout(() => this.startScroll(), 500))
+        }
+      } catch (e) {
+        console.error('RiskWarning:', e)
+      }
+    },
     startScroll() {
       const wrap = this.$refs.scrollWrap
       const inner = this.$refs.scrollInner

+ 22 - 3
src/components/LabStats/SafetyLevel.vue

@@ -16,17 +16,36 @@ import { getSafetyLevelStats } from '@/api'
 export default {
   name: 'SafetyLevel',
   data() {
-    return { chart: null, scrollTimer: null }
+    return { chart: null, scrollTimer: null, timer: null }
   },
   async mounted() {
-    const res = await getSafetyLevelStats()
-    this.$nextTick(() => this.initChart(res.data))
+    await this.loadData()
+    this.timer = setInterval(this.loadData, 5 * 60 * 1000)
   },
   beforeDestroy() {
     if (this.chart) this.chart.dispose()
     if (this.scrollTimer) clearInterval(this.scrollTimer)
+    clearInterval(this.timer)
   },
   methods: {
+    async loadData() {
+      try {
+        const res = await getSafetyLevelStats()
+        if (this.chart) {
+          const depts = res.data.departments
+          this.chart.setOption({ xAxis: { data: depts.map(d => d.name) }, series: [
+            { data: depts.map(d => d.level1) },
+            { data: depts.map(d => d.level2) },
+            { data: depts.map(d => d.level3) },
+            { data: depts.map(d => d.level4) }
+          ]})
+        } else {
+          this.$nextTick(() => this.initChart(res.data))
+        }
+      } catch (e) {
+        console.error('SafetyLevel:', e)
+      }
+    },
     initChart(data) {
       this.chart = echarts.init(this.$refs.chart)
       const depts = data.departments

+ 6 - 2
src/components/VideoMonitor/BuildingNav.vue

@@ -36,8 +36,12 @@ export default {
     return { treeData: [] }
   },
   async mounted() {
-    const res = await getBuildingTree()
-    this.treeData = res.data.tree
+    try {
+      const res = await getBuildingTree()
+      this.treeData = res.data.tree
+    } catch (e) {
+      console.error('BuildingNav:', e)
+    }
   },
   methods: {
     handleNodeClick(data) {

+ 7 - 3
src/components/VideoMonitor/VideoGrid.vue

@@ -67,9 +67,13 @@ export default {
   },
   methods: {
     async loadVideos() {
-      const res = await getVideoList({ page: this.currentPage })
-      this.videoList = res.data.list
-      this.total = res.data.total
+      try {
+        const res = await getVideoList({ page: this.currentPage })
+        this.videoList = res.data.list
+        this.total = res.data.total
+      } catch (e) {
+        console.error('VideoGrid:', e)
+      }
     },
     changePage(delta) {
       this.currentPage += delta

+ 20 - 4
src/utils/request.js

@@ -1,8 +1,11 @@
 import axios from 'axios'
+import router from '@/router'
+import store from '@/store'
+import { Message } from 'element-ui'
 
 const service = axios.create({
-  baseURL: '/api',
-  timeout: 10000
+  baseURL: process.env.VUE_APP_BASE_API || '',
+  timeout: Number(process.env.VUE_APP_TIMEOUT) || 15000
 })
 
 // 请求拦截器
@@ -10,7 +13,7 @@ service.interceptors.request.use(
   config => {
     const token = localStorage.getItem('token')
     if (token) {
-      config.headers['Authorization'] = 'Bearer ' + token
+      config.headers['Authorization'] = token
     }
     return config
   },
@@ -20,7 +23,20 @@ service.interceptors.request.use(
 // 响应拦截器
 service.interceptors.response.use(
   response => response.data,
-  error => Promise.reject(error)
+  error => {
+    if (error.response && error.response.status === 401) {
+      store.dispatch('logout')
+      if (router.currentRoute.path !== '/login') {
+        router.push('/login')
+      }
+    } else {
+      const msg = error.response && error.response.data && error.response.data.message
+        ? error.response.data.message
+        : error.message || '请求失败'
+      Message.error(msg)
+    }
+    return Promise.reject(error)
+  }
 )
 
 export default service

+ 2 - 4
src/views/Login.vue

@@ -95,14 +95,12 @@ export default {
 
         this.loading = true
         try {
-          // 密码使用md5加密后提交
           const params = {
-            username: this.form.username,
+            account: this.form.username,
             password: md5(this.form.password)
           }
           const res = await loginApi(params)
-          // 保存token到Vuex
-          this.$store.dispatch('login', res.data)
+          this.$store.dispatch('login', { token: res.data.token, userInfo: {} })
           this.$message.success('登录成功')
           this.$router.push('/lab-status')
         } catch (err) {

+ 7 - 2
vue.config.js

@@ -9,10 +9,15 @@ module.exports = defineConfig({
     port: 8080,
     open: true,
     proxy: {
-      '/api': {
+      '/auth': {
         target: process.env.VUE_APP_BASE_API,
         changeOrigin: true,
-        pathRewrite: { '^/api': '' }
+        pathRewrite: { '^/auth': '/auth' }
+      },
+      '/laboratory': {
+        target: process.env.VUE_APP_BASE_API,
+        changeOrigin: true,
+        pathRewrite: { '^/laboratory': '/laboratory' }
       }
     }
   },