Kaynağa Gözat

修复异常

dedsudiyu 6 gün önce
ebeveyn
işleme
ad55d52c10

+ 3 - 1
.claude/settings.local.json

@@ -1,7 +1,9 @@
 {
   "permissions": {
     "allow": [
-      "Bash(npm run:*)"
+      "Bash(npm run:*)",
+      "Bash(node -e \"require\\(''buffer''\\); require\\(''process/browser''\\); require\\(''url/''\\)\")",
+      "Bash(npm install:*)"
     ]
   }
 }

+ 7 - 7
.env.development

@@ -1,11 +1,11 @@
 # 开发环境配置
-VUE_APP_TITLE=实验室安全智能监测与管控中心
+NODE_ENV=development
 
-# 后端接口基础地址(开发环境走代理,此处填 /dev-api)
-VUE_APP_BASE_API=/dev-api
+# 项目名称
+VUE_APP_TITLE=中国安全生产科学研究院实验室安全智慧化管控中心
 
-# laboratory 服务代理目标(改为实际后端地址)
-VUE_APP_LAB_TARGET=http://192.168.1.8/api
+# 接口基础地址
+VUE_APP_BASE_API=http://192.168.1.8/api
 
-# auth 服务代理目标(改为实际后端地址
-VUE_APP_AUTH_TARGET=http://192.168.1.8/api
+# 接口超时时间(毫秒
+VUE_APP_TIMEOUT=10000

+ 9 - 3
.env.production

@@ -1,5 +1,11 @@
 # 生产环境配置
-VUE_APP_TITLE=实验室安全智能监测与管控中心
+NODE_ENV=production
 
-# 生产环境接口地址(直接写后端域名,不走本地代理)
-VUE_APP_BASE_API=https://your-production-domain.com
+# 项目名称
+VUE_APP_TITLE=中国安全生产科学研究院实验室安全智慧化管控中心
+
+# 接口基础地址
+VUE_APP_BASE_API=http://192.168.1.8/api
+
+# 接口超时时间(毫秒)
+VUE_APP_TIMEOUT=15000

Dosya farkı çok büyük olduğundan ihmal edildi
+ 5122 - 2303
package-lock.json


+ 3 - 0
package.json

@@ -21,8 +21,11 @@
     "@vue/cli-plugin-router": "~5.0.8",
     "@vue/cli-plugin-vuex": "~5.0.8",
     "@vue/cli-service": "~5.0.8",
+    "buffer": "^6.0.3",
+    "process": "^0.11.10",
     "sass": "~1.32.13",
     "sass-loader": "^12.0.0",
+    "url": "^0.11.4",
     "vue-template-compiler": "^2.7.16"
   },
   "browserslist": [

+ 10 - 2
src/components/AlarmInfo.vue

@@ -50,12 +50,20 @@ export default {
   computed: {
     /** Duplicate data for seamless CSS scroll loop */
     scrollList() {
-      return [...this.warningList, ...this.warningList]
+      if(this.warningList[6]){
+        return [...this.warningList, ...this.warningList]
+      }else if(this.warningList[0]){
+        return [...this.warningList]
+      }else{
+        return []
+      }
     }
   },
   mounted() {
     this.fetchData()
-    this.pollTimer = setInterval(this.fetchData, 5 * 60 * 1000)
+    if(this.warningList[6]){
+      this.pollTimer = setInterval(this.fetchData, 5 * 60 * 1000)
+    }
   },
   beforeDestroy() {
     if (this.pollTimer) clearInterval(this.pollTimer)

+ 40 - 30
src/components/LabEnvironment.vue

@@ -20,20 +20,21 @@
           v-for="(item, idx) in scrollList"
           :key="idx"
           class="sensor-item"
-          :class="{ alert: item.alert }"
+          :class="{ alert: item.hasException }"
         >
           <div class="sensor-item-head">
-            <span class="sensor-name">{{ item.name }}({{ item.room }})</span>
+            <span class="sensor-name">{{ item.subName }}({{ item.roomNum }})</span>
             <span class="sensor-unit">{{ item.unit }}</span>
-            <span v-if="item.alert" class="sensor-status alarm-status">&#x1F6A8; 告警</span>
+            <span v-if="item.hasException" 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 class="sensor-metric" v-for="(minItem, mIdx) in item.sensorList" :key="mIdx">
+              <span class="span-svg">
+                <svgIcon  icon-class="http://192.168.1.8/statics/2026/03/24/d3d2315f-4851-4563-8257-0bc64f08718f.svg"/>
+              </span>
+              <span class="span-text">{{ minItem.attributeName }} {{ minItem.deviceValue }}{{ minItem.unit }}</span>
+            </div>
           </div>
         </div>
       </div>
@@ -43,9 +44,13 @@
 
 <script>
 import { getEnvMonitor } from '@/api/screen'
+import svgIcon from '@/components/svgIcon.vue'
 
 export default {
   name: 'LabEnvironment',
+  components: {
+    svgIcon,
+  },
   data() {
     return {
       sensorList: [],
@@ -55,12 +60,20 @@ export default {
   computed: {
     /** Duplicate data for seamless CSS scroll loop */
     scrollList() {
-      return [...this.sensorList, ...this.sensorList]
+      if(this.sensorList[12]){
+        return [...this.sensorList, ...this.sensorList]
+      }else if(this.sensorList[0]){
+        return [...this.sensorList]
+      }else{
+        return []
+      }
     }
   },
   mounted() {
     this.fetchData()
-    this.pollTimer = setInterval(this.fetchData, 5 * 60 * 1000)
+    if(this.sensorList[12]){
+      this.pollTimer = setInterval(this.fetchData, 5 * 60 * 1000)
+    }
   },
   beforeDestroy() {
     if (this.pollTimer) clearInterval(this.pollTimer)
@@ -71,26 +84,7 @@ export default {
         const res = await getEnvMonitor({ page: 1, pageSize: 20 })
         if (res.code === 200) {
           const records = (res.data.records || [])
-          const list = []
-          records.forEach(lab => {
-            const sensors = lab.sensorList || []
-            const get = (code) => {
-              const s = sensors.find(s => s.code === code)
-              return s ? s.deviceValue : null
-            }
-            list.push({
-              name:  lab.subName,
-              room:  lab.roomNum,
-              unit:  '',
-              t:     get('temperature'),
-              h:     get('humidity'),
-              tvoc:  get('tvoc'),
-              co2:   get('co2'),
-              o2:    get('o2'),
-              alert: lab.hasException
-            })
-          })
-          this.sensorList = list
+          this.sensorList = records
         }
       } catch (e) {
         // 错误已由拦截器处理
@@ -336,4 +330,20 @@ export default {
     animation: blinkRed 0.8s ease-in-out infinite;
   }
 }
+
+.span-svg {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 28px;
+  height: 28px;
+  flex-shrink: 0;
+
+  img {
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+    color:#fff;
+  }
+}
 </style>

+ 1 - 1
src/components/ScreenHeader.vue

@@ -114,7 +114,7 @@ export default {
 /* ========== TOP NAV ========== */
 .screen-header {
   position: relative;
-  z-index: 10;
+  z-index: 1;
   flex-shrink: 0;
   width: 100%;
   height: 160px;

+ 12 - 14
src/components/SecurityMonitor.vue

@@ -337,8 +337,6 @@ export default {
     },
     //手动加载
     loadNode(node, resolve) {
-      console.log('node',node);
-      console.log('resolve',resolve);
       let self = this;
       if (node.data){
         if(node.data.level == 3){
@@ -472,18 +470,18 @@ export default {
       try {
         const res = await getCameraStream(this.videoQueryParams)
         let list = [];
-        res.data.total = 100;
-        res.data.records = [
-          {streamUrl:'1', deviceNo:'1'},
-          {streamUrl:'2', deviceNo:'2'},
-          {streamUrl:'3', deviceNo:'3'},
-          {streamUrl:'4', deviceNo:'4'},
-          {streamUrl:'5', deviceNo:'5'},
-          {streamUrl:'6', deviceNo:'6'},
-          {streamUrl:'7', deviceNo:'7'},
-          {streamUrl:'8', deviceNo:'8'},
-          {streamUrl:'9', deviceNo:'9'},
-        ];
+        // res.data.total = 100;
+        // res.data.records = [
+        //   {streamUrl:'1', deviceNo:'1'},
+        //   {streamUrl:'2', deviceNo:'2'},
+        //   {streamUrl:'3', deviceNo:'3'},
+        //   {streamUrl:'4', deviceNo:'4'},
+        //   {streamUrl:'5', deviceNo:'5'},
+        //   {streamUrl:'6', deviceNo:'6'},
+        //   {streamUrl:'7', deviceNo:'7'},
+        //   {streamUrl:'8', deviceNo:'8'},
+        //   {streamUrl:'9', deviceNo:'9'},
+        // ];
         for(let i=0;i<res.data.records.length;i++){
           list.push(
             {

+ 61 - 0
src/components/svgIcon.vue

@@ -0,0 +1,61 @@
+<template>
+  <div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
+  <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
+    <use :xlink:href="iconName" />
+  </svg>
+</template>
+
+<script>
+import { isExternal } from '@/utils/validate'
+
+export default {
+  name: 'SvgIcon',
+  props: {
+    iconClass: {
+      type: String,
+      required: true
+    },
+    className: {
+      type: String,
+      default: ''
+    }
+  },
+  computed: {
+    isExternal() {
+      return isExternal(this.iconClass)
+    },
+    iconName() {
+      return `#icon-${this.iconClass}`
+    },
+    svgClass() {
+      if (this.className) {
+        return 'svg-icon ' + this.className
+      } else {
+        return 'svg-icon'
+      }
+    },
+    styleExternalIcon() {
+      return {
+        mask: `url(${this.iconClass}) no-repeat 50% 50%`,
+        '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.svg-icon {
+  width: 1em;
+  height: 1em;
+  vertical-align: -0.15em;
+  fill: currentColor;
+  overflow: hidden;
+}
+
+.svg-external-icon {
+  background-color: currentColor;
+  mask-size: cover!important;
+  display: inline-block;
+}
+</style>

+ 15 - 33
src/utils/request.js

@@ -1,19 +1,19 @@
 import axios from 'axios'
-import { Message } from 'element-ui'
 import router from '@/router'
+import store from '@/store'
+import { Message } from 'element-ui'
 
-// 创建 axios 实例
 const service = axios.create({
-  baseURL: process.env.VUE_APP_BASE_API || '/dev-api',
-  timeout: 15000
+  baseURL: process.env.VUE_APP_BASE_API || '',
+  timeout: Number(process.env.VUE_APP_TIMEOUT) || 15000
 })
 
-// 请求拦截器 —— 注入 token
+// 请求拦截器
 service.interceptors.request.use(
   config => {
     const token = localStorage.getItem('token')
     if (token) {
-      config.headers['Authorization'] = `${token}`
+      config.headers['Authorization'] = token
     }
     return config
   },
@@ -22,36 +22,18 @@ service.interceptors.request.use(
 
 // 响应拦截器
 service.interceptors.response.use(
-  response => {
-    const res = response.data
-    // 业务层 401:token 失效
-    if (res.code === 401) {
-      localStorage.removeItem('token')
-      Message.error('登录已过期,请重新登录')
-      if (router.currentRoute.path !== '/login') {
-        router.replace('/login')
-      }
-      return Promise.reject(new Error('Unauthorized'))
-    }
-    return res
-  },
+  response => response.data,
   error => {
-    if (error.response) {
-      const { status, data } = error.response
-      if (status === 401) {
-        // HTTP 401:token 失效,跳回登录页
-        localStorage.removeItem('token')
-        Message.error('登录已过期,请重新登录')
-        if (router.currentRoute.path !== '/login') {
-          router.replace('/login')
-        }
-      } else {
-        // 其他错误:优先展示后端 message,否则展示 HTTP 状态
-        const msg = (data && data.message) || `请求失败(${status})`
-        Message.error(msg)
+    if (error.response && error.response.status === 401) {
+      store.dispatch('logout')
+      if (router.currentRoute.path !== '/login') {
+        router.push('/login')
       }
     } else {
-      Message.error('网络异常,请检查连接')
+      const msg = error.response && error.response.data && error.response.data.message
+        ? error.response.data.message
+        : error.message || '请求失败'
+      Message.error(msg)
     }
     return Promise.reject(error)
   }

+ 83 - 0
src/utils/validate.js

@@ -0,0 +1,83 @@
+/**
+ * @param {string} path
+ * @returns {Boolean}
+ */
+export function isExternal(path) {
+  return /^(https?:|mailto:|tel:)/.test(path)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validUsername(str) {
+  const valid_map = ['admin', 'editor']
+  return valid_map.indexOf(str.trim()) >= 0
+}
+
+/**
+ * @param {string} url
+ * @returns {Boolean}
+ */
+export function validURL(url) {
+  const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
+  return reg.test(url)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validLowerCase(str) {
+  const reg = /^[a-z]+$/
+  return reg.test(str)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validUpperCase(str) {
+  const reg = /^[A-Z]+$/
+  return reg.test(str)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validAlphabets(str) {
+  const reg = /^[A-Za-z]+$/
+  return reg.test(str)
+}
+
+/**
+ * @param {string} email
+ * @returns {Boolean}
+ */
+export function validEmail(email) {
+  const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+  return reg.test(email)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function isString(str) {
+  if (typeof str === 'string' || str instanceof String) {
+    return true
+  }
+  return false
+}
+
+/**
+ * @param {Array} arg
+ * @returns {Boolean}
+ */
+export function isArray(arg) {
+  if (typeof Array.isArray === 'undefined') {
+    return Object.prototype.toString.call(arg) === '[object Array]'
+  }
+  return Array.isArray(arg)
+}

+ 44 - 15
vue.config.js

@@ -1,27 +1,26 @@
 const { defineConfig } = require('@vue/cli-service')
+const webpack = require('webpack')
 
 module.exports = defineConfig({
   transpileDependencies: true,
   lintOnSave: false,
-
-  // 生产环境不生成 source map,减小包体积
   productionSourceMap: false,
+  publicPath: './',
 
   devServer: {
     port: 8080,
     open: true,
+    historyApiFallback: true,
     proxy: {
-      // /dev-api/auth/** → auth 服务
-      '/dev-api/auth': {
-        target: process.env.VUE_APP_AUTH_TARGET || 'http://localhost:8080',
+      '/auth': {
+        target: process.env.VUE_APP_BASE_API,
         changeOrigin: true,
-        pathRewrite: { '^/dev-api': '' }
+        pathRewrite: { '^/auth': '/auth' }
       },
-      // /dev-api/laboratory/** → laboratory 服务
-      '/dev-api/laboratory': {
-        target: process.env.VUE_APP_LAB_TARGET || 'http://localhost:8080',
+      '/laboratory': {
+        target: process.env.VUE_APP_BASE_API,
         changeOrigin: true,
-        pathRewrite: { '^/dev-api': '' }
+        pathRewrite: { '^/laboratory': '/laboratory' }
       }
     }
   },
@@ -34,26 +33,56 @@ module.exports = defineConfig({
     }
   },
 
-  // 按需加载:将第三方库单独分包
+  chainWebpack(config) {
+    // 页面标题
+    config.plugin('html').tap(args => {
+      args[0].title = process.env.VUE_APP_TITLE || '实验室安全智慧化管控中心'
+      return args
+    })
+  },
+
   configureWebpack: {
+    plugins: [
+      new webpack.ProvidePlugin({
+        Buffer: ['buffer', 'Buffer'],
+        process: 'process/browser'
+      })
+    ],
+    resolve: {
+      fallback: {
+        url: require.resolve('url/'),
+        buffer: require.resolve('buffer/'),
+        process: require.resolve('process/browser')
+      }
+    },
     optimization: {
       splitChunks: {
         chunks: 'all',
         cacheGroups: {
+          // Vue 全家桶
+          vue: {
+            name: 'chunk-vue',
+            test: /[\\/]node_modules[\\/](vue|vue-router|vuex)[\\/]/,
+            priority: 30
+          },
+          // ECharts 单独拆包(体积最大)
           echarts: {
             name: 'chunk-echarts',
-            test: /[\\/]node_modules[\\/]echarts[\\/]/,
-            priority: 20
+            test: /[\\/]node_modules[\\/](echarts|zrender)[\\/]/,
+            priority: 25
           },
+          // Element UI
           elementUI: {
             name: 'chunk-element-ui',
             test: /[\\/]node_modules[\\/]element-ui[\\/]/,
-            priority: 15
+            priority: 20
           },
+          // 其余第三方
           vendors: {
             name: 'chunk-vendors',
             test: /[\\/]node_modules[\\/]/,
-            priority: 10
+            priority: 10,
+            reuseExistingChunk: true
           }
         }
       }