|
|
<template> <div class="env-container"> <div class="env-top-title"> <img src="@/assets/images/logo-2.png" alt=""> <p>档案库房智能管理系统</p> </div> <div class="header-date"> <div class="time">{{ nowDate.split(' ')[1] }}</div> <div class="time-other"> <span>{{ currentWeek }}</span> <span>{{ nowDate.split(' ')[0] }}</span> </div> </div> <div class="env-main"> <div class="env-main-left"> <div class="env-item container-wrap" style="height: 130px !important; "> <span class="right-top-line" /> <span class="left-bottom-line" /> <h3> <svg-icon icon-class="danganjieyue" style="margin-right:10px" />档案借阅 </h3> <div class="chart-wrapper"> <lend-across :lend-data="lendData" :refreshtime="refreshtime" /> </div> </div> <div class="env-item container-wrap left-wrap"> <span class="right-top-line" /> <span class="left-bottom-line" /> <h3> <i class="iconfont icon-kongqizhiliangshuju" />环控数据 </h3> <!-- <ul class="leakage-list" :style="{ height: showScroll ? 'calc(100% - 40px)' : 'auto', overflow: showScroll ? 'auto' : 'hidden' }" > <li v-for="item in validDisplayConfigData" :key="item.id" :class="{ 'leakage-warn': item.NetStatus === 0 }" :style="{ width: liWidth, height: liHeight, marginRight: '12px', marginBottom: '12px' }" > <p><i class="iconfont icon-shebei" />{{ item.Name }}</p> <span class="leakage-state-tip" /> </li> </ul> --> <ul v-if="newAlarm && newAlarm.length !== 0" class="screen-env-list"> <!-- <li :class="alarmStatus.infrared === '告警' ? 'li-warn alarm-status' : 'alarm-status'"> <div class="msg-txt">
<p class="msg-list-unit">红外 </p> <span class="msg-list-num">{{ alarmStatus.infrared }}</span> </div> </li> <li :class="alarmStatus.fire === '告警' ? 'li-warn alarm-status' : 'alarm-status'"> <div class="msg-txt"> <p class="msg-list-unit">消防 </p> <span class="msg-list-num">{{ alarmStatus.fire }}</span> </div> </li> <li :class="alarmStatus.waterLeak === '告警' ? 'li-warn alarm-status' : 'alarm-status'"> <div class="msg-txt"> <p class="msg-list-unit">漏水 </p> <span class="msg-list-num">{{ alarmStatus.waterLeak }}</span> </div> </li>--> <li> <svg-icon icon-class="pm25" class-name="msg-list-svg" /> <div class="msg-txt"> <span class="msg-list-num">{{ avgData.pm25 }}</span> <p class="msg-list-unit">PM2.5浓度 <br>{{ avgData.pm25Unit }}</p> </div> </li> <li> <svg-icon icon-class="pm10" class-name="msg-list-svg" /> <div class="msg-txt"> <span class="msg-list-num">{{ avgData.pm10 }}</span> <p class="msg-list-unit">PM10浓度 <br>{{ avgData.pm10Unit }}</p> </div> </li> <li> <svg-icon icon-class="voc" class-name="msg-list-svg" /> <div class="msg-txt"> <span class="msg-list-num">{{ avgData.tvoc }}</span> <p class="msg-list-unit">TVOC {{ avgData.tvocUnit }}</p> </div> </li> <li> <svg-icon icon-class="co2" class-name="msg-list-svg" /> <div class="msg-txt"> <span class="msg-list-num">{{ avgData.co2 }}</span> <p class="msg-list-unit">二氧化碳 {{ avgData.co2Unit }}</p> </div> </li> <li> <svg-icon icon-class="jiaquan" class-name="msg-list-svg" style="font-size: 68px;" /> <div class="msg-txt"> <span class="msg-list-num">{{ avgData.formaldehyde }}</span> <p class="msg-list-unit">甲醛 {{ avgData.formaldehydeUnit }}</p> </div> </li> </ul> </div> <div class="env-item container-wrap left-wrap"> <span class="right-top-line" /> <span class="left-bottom-line" /> <h3> <i class="iconfont icon-kongqizhiliangshuju" />设备联调状态 </h3> <div style="display: flex; justify-content: space-between; height: calc(100% - 40px);"> <ul v-if="hasValidData" class="leakage-list"> <li :class="alarmStatus.infrared === '告警' ? 'leakage-warn' : ''"> <p><i class="iconfont icon-shebei" />红外</p> <span class="leakage-state-tip" /> </li> <li :class="alarmStatus.fire === '告警' ? 'leakage-warn' : ''"> <p><i class="iconfont icon-shebei" />消防</p> <span class="leakage-state-tip" /> </li> <li :class="alarmStatus.waterLeak === '告警' ? 'leakage-warn' : ''"> <p><i class="iconfont icon-shebei" />漏水</p> <span class="leakage-state-tip" /> </li> </ul> <ul class="leakage-list"> <li v-for="item in validDisplayConfigData.slice().sort((a, b) => { // 空值处理
if (!a.Name) return 1; if (!b.Name) return -1; // 按长度升序,长度相同则按名称排序
if (a.Name.length === b.Name.length) { return a.Name.localeCompare(b.Name, 'zh-CN', { numeric: true }); } return a.Name.length - b.Name.length; // 升序;降序则 b.Name.length - a.Name.length
})" :key="item.id" :class="{ 'leakage-warn': item.NetStatus === 0 }" > <p><i class="iconfont icon-shebei" />{{ item.Name }}</p> <span class="leakage-state-tip" /> </li> </ul> </div> </div> </div> <div class="env-main-middle"> <div class="env-3d"> <iframe id="myIframe" ref="myIframe" name="iframeMap" class="iframe_box" src="/web3D/index.html" frameborder="0" scrolling="no" style=" margin: 0 auto; display: block;" /> </div> <div class="env-alarm-container"> <ul v-if="hasValidData" class="env-alarm-list env-alarm-list-first"> <li> <svg-icon icon-class="temperature" class-name="msg-list-svg" /> <div> <span>{{ avgData.temperature }} </span> <p>温度 {{ avgData.temperatureUnit }}</p> </div> </li> <li> <svg-icon icon-class="shidu" class-name="msg-list-svg" /> <div> <span>{{ avgData.humidity }}</span> <p>湿度 {{ avgData.humidityUnit }}</p> </div> </li> </ul> <div v-if="hasValidData" class="air-quality" :class="[ aqiStatus === '优' ? 'air-excellent' : '', aqiStatus === '良' ? 'air-good' : '', aqiStatus === '轻度污染' ? 'air-lightPollution' : '', aqiStatus === '中度污染' ? 'air-mediumPollution' : '', aqiStatus === '重度污染' ? 'air-heavyPollution' : '', aqiStatus === '严重污染' ? 'air-severePollution' : '' ]" > <h3>空气质量指数</h3> <div class="air-params"> <div class="air-left"> <span class="air-title">实时AQI</span> <div class="air-result">{{ aqiValue }}</div> </div> <div class="air-right"> <span>空气质量</span> <!-- <p>{{ aqiStatus }}</p> --> <p class="air-status-text" v-html="formatAqiStatus" /> </div> </div> </div> <!-- <el-row :gutter="10" class="panel-group" type="flex" justify="space-between"> <el-col class="card-panel-col"> <div class="card-panel zaixianshebei"> <div class="card-panel-icon-wrapper icon-shopping"> <svg-icon icon-class="zaixianshebei" class-name="card-panel-icon" /> </div> <div class="card-panel-description"> <div class="card-panel-text"> <count-to v-if="getDeviceFlag" :start-val="0" :end-val="onlineDeviceNum" :duration="3200" class="card-panel-num" /> <div v-if="!getDeviceFlag" class="card-panel-text"><span class="card-panel-num">获取中...</span></div> </div> 在线设备 </div> </div> </el-col> <el-col class="card-panel-col"> <div class="card-panel lixianshebei"> <div class="card-panel-icon-wrapper icon-shopping"> <svg-icon icon-class="lixianshebei" class-name="card-panel-icon" /> </div> <div class="card-panel-description"> <div class="card-panel-text"> <count-to v-if="getDeviceFlag" :start-val="0" :end-val="offlineDeviceNum" :duration="3200" class="card-panel-num" /> <div v-if="!getDeviceFlag" class="card-panel-text"><span class="card-panel-num">获取中...</span></div> </div> 离线设备 </div> </div> </el-col> </el-row> --> <ul class="env-alarm-list env-alarm-list-first"> <li> <svg-icon icon-class="zaixian" class-name="msg-list-svg" /> <div> <!-- <span>{{ avgData.temperature }} </span> --> <count-to v-if="getDeviceFlag" :start-val="0" :end-val="onlineDeviceNum" :duration="3200" class="card-panel-num" /> <div v-if="!getDeviceFlag" class="card-panel-text"><span class="card-panel-num">获取中...</span></div> <p>在线设备</p> </div> </li> <li> <svg-icon icon-class="lixian" class-name="msg-list-svg" /> <div> <!-- <span>{{ avgData.humidity }}</span> --> <count-to v-if="getDeviceFlag" :start-val="0" :end-val="offlineDeviceNum" :duration="3200" class="card-panel-num" /> <div v-if="!getDeviceFlag" class="card-panel-text"><span class="card-panel-num">获取中...</span></div> <p>离线设备</p> </div> </li> </ul> </div> </div> <div class="env-main-right"> <!-- 环控实时报警 --> <warehouse-warning :height="'calc(100% - 38px)'" /> <!-- 门禁出入记录 --> <AccessDoor :height="'calc(100% - 40px)'" size="4" /> <!-- 当日告警统计 --> <div class="env-item container-wrap"> <span class="right-top-line" /> <span class="left-bottom-line" /> <h3> <svg-icon icon-class="alerm" class-name="warehouse-svg" />当日告警统计 </h3> <div class="chart-wrapper" style="height: calc(100% - 40px);"> <catePie :cate-data="alarmChartData" :refreshtime="refreshtime" /> </div> </div> </div> </div> </div></template>
<script>import CountTo from 'vue-count-to'import { getCurrentTime } from '@/utils/index'import lendAcross from '@/views/components/echarts/lendAcross.vue'import catePie from './module/catePie.vue'import WarehouseWarning from '@/views/components/WarehouseWarning'import AccessDoor from '@/views/components/AccessDoor'import { statisticsCrud } from '@/views/system/archiveStatistics/mixins/statistics'import displayConfigApi from '@/api/storeManage/displayConfig'import { getDeviceOnoff, getTodayHikAlarmLog } from '@/api/home/device'import alarmApi from '@/api/home/alarm'// import { allDeviceData, mockIpData } from './index.js'
// const mockFetchDataForIP = (params) => {
// return new Promise((resolve) => {
// setTimeout(() => {
// const ip = params.ip
// const result = mockIpData[ip] || { code: 200, message: '操作成功', data: [], timestamp: Date.now() }
// resolve(result.data)
// }, 500)
// })
// }
export default { name: 'EnvironmentalScreen', components: { CountTo, WarehouseWarning, AccessDoor, lendAcross, catePie }, mixins: [statisticsCrud], data() { return { bannerRoomName: '3F 全景图', nowDate: '', currentWeek: this.getCurrentWeek(), timer: null, echartsTimer: null, roomId: 'D6490DA3D4261E8C26D0E3', allDisplayConfigData: [], displayConfigData: [], url: '', allDeviceIds: [], // 所有设备IP(无排除)
refreshtime: 60000, lendData: [], typeData: [], newAlarm: [], // 所有设备的原始数据
aqiValue: 45, aqiStatus: '健康', // 需展示的指标列表(包含所有要计算的指标)
keepIndicators: [ '二氧化碳', 'CO2浓度', '甲醛', '综合气体', 'PM2.5浓度', 'PM10浓度', '温度', '湿度', '空气质量', 'TVOC', '红外', '消防', '漏水' ], iframeWin: null, // 平均值数据(新增温湿度和单位字段)
avgData: { temperature: '0.00', // 温度
temperatureUnit: '', // 温度单位
humidity: '0.00', // 湿度
humidityUnit: '', // 湿度单位
pm25: '0.00', pm25Unit: '', // PM2.5单位
tvoc: '0.00', tvocUnit: '', // TVOC单位
pm10: '0.00', pm10Unit: '', // PM10单位
co2: '0.00', co2Unit: '', // 二氧化碳单位
formaldehyde: '0.00', formaldehydeUnit: '' // 甲醛单位
}, // 告警状态(ON=告警,OFF=正常)
alarmStatus: { infrared: '正常', fire: '正常', waterLeak: '正常' }, aqiGradeStandard: [ { aqiMin: 0, aqiMax: 50, level: '优', colorKey: 'excellent' }, { aqiMin: 51, aqiMax: 100, level: '良', colorKey: 'good' }, { aqiMin: 101, aqiMax: 150, level: '轻度污染', colorKey: 'lightPollution' }, { aqiMin: 151, aqiMax: 200, level: '中度污染', colorKey: 'mediumPollution' }, { aqiMin: 201, aqiMax: 300, level: '重度污染', colorKey: 'heavyPollution' }, { aqiMin: 301, aqiMax: 500, level: '严重污染', colorKey: 'severePollution' } ], pollutantLimits: { pm25: { excellent: 35, good: 75, light: 115, medium: 150, heavy: 250, severe: 500 }, // μg/m³
pm10: { excellent: 50, good: 150, light: 250, medium: 350, heavy: 420, severe: 600 } // μg/m³
}, hasValidData: false, // 是否有有效数据
hkConfig: { 'username': 'admin', 'password': 'ftzn83560792', 'ip': '192.168.99.125', // 固定为目标IP
'port': '554' }, webRtcServer: null, camera_ip: '127.0.0.1:9527', cameraList: [], // 摄像头列表
targetDeviceIps: ['192.168.99.101:5003', '192.168.99.101:6003', '192.168.99.101:5004'], getDeviceFlag: false, totalDeviceNum: 0, onlineDeviceNum: 0, offlineDeviceNum: 0, alarmStatisticsRaw: [], // 原始告警统计数据
alarmChartData: [], // 格式化后给饼图的数据源
alarmRefreshTimer: null // 告警数据刷新定时器
} }, computed: { // 处理空气质量状态换行的计算属性
formatAqiStatus() { const status = this.aqiStatus // 仅当文本长度为4时,在第2个字后插入<br/>换行
if (status && status.length === 4) { return status.slice(0, 2) + '<br/>' + status.slice(2) } // 其他长度(2字/3字)直接返回原文本
return status || '' }, validDisplayConfigData() { return this.allDisplayConfigData.filter(item => { return item && item.Name && this.targetDeviceIps.includes(item.IP?.trim()) }) }, itemsPerRow() { const len = this.validDisplayConfigData.length if (len === 0) return 0 return len <= 2 ? len : 2 }, liWidth() { if (this.itemsPerRow === 0) return '0' return `calc(100% / ${this.itemsPerRow} - 12px)` }, liHeight() { const len = this.validDisplayConfigData.length if (len === 0) return '0' const rows = Math.ceil(len / this.itemsPerRow) return `calc(100% / ${rows} - 12px)` }, showScroll() { return this.validDisplayConfigData.length > 8 } }, async created() { // 时间更新定时器
this.timer = setInterval(() => { this.nowDate = getCurrentTime() }, 1000) this.getDevice() this.getTodayHikAlarmLog() // 初始化
window.getIframeLoading = this.getIframeLoading // this.allDisplayConfigData = allDeviceData
// this.handleDeviceIpList()
await alarmApi.FetchYpGetSite().then((data) => { if (data && data.length > 0) { this.allDisplayConfigData = data this.handleDeviceIpList() } else { this.allDisplayConfigData = [] } })
if (this.allDeviceIds.length > 0) { await this.getAllDevicesData() this.calcAllAvgData() // 计算所有指标平均值
this.calcAQIByAvg() // 根据平均值计算AQI
} else { console.warn('无设备IP数据') this.hasValidData = false } }, mounted() { this.iframeWin = this.$refs.myIframe?.contentWindow // Echarts数据刷新定时器
this.echartsTimer = setInterval(() => { this.lendData = [] this.typeData = [] this.alarmChartData = [] this.getBorrowerNumSta() this.getTodayHikAlarmLog() }, this.refreshtime) // this.getVideoUrl()
}, beforeDestroy() { // 清理所有定时器
if (this.timer) clearInterval(this.timer) if (this.echartsTimer) clearInterval(this.echartsTimer) // 销毁视频流
if (this.webRtcServer) { this.webRtcServer.disconnect() this.webRtcServer = null } // if (this.alarmRefreshTimer) clearInterval(this.alarmRefreshTimer)
}, methods: { getTodayHikAlarmLog() { getTodayHikAlarmLog().then(data => { console.log('今日海康告警日志', data) this.alarmStatisticsRaw = this.transformAlarmData(data || {}) this.formatAlarmChartData() }).catch(error => { console.error('获取告警统计数据失败:', error) this.alarmStatisticsRaw = [] this.alarmChartData = [] }) },
transformAlarmData(alarmObj) { const fieldMap = [ { key: 'accessAlarm', name: '门禁告警' }, { key: 'firefightingAlarm', name: '消防告警' }, { key: 'equipmentAlarm', name: '设备告警' }, { key: 'environmentAlarm', name: '环境告警' } ]
return fieldMap.map(item => ({ alarmValue: item.name, num: alarmObj[item.key] || 0 })) },
formatAlarmChartData() { // const allZero = this.alarmStatisticsRaw.every(item => item.num === 0)
// if (allZero) {
// this.alarmChartData = [{ name: '无告警', value: 1, itemStyle: { color: '#666666' }}]
// return
// }
const alarmColorMap = { '门禁告警': '#F65164', '消防告警': '#FFB800', '设备告警': '#339CFF', '环境告警': '#1CADAB' } // 备用颜色列表(防止新增告警类型没有配置颜色)
const colorList = ['#F65164', '#339CFF', '#FFB800', '#1CADAB', '#9B6BCC', '#FF7D00']
this.alarmChartData = this.alarmStatisticsRaw.map((item, index) => { const alarmName = item.alarmValue // 优先使用配置的颜色,没有则使用备用颜色
const color = alarmColorMap[alarmName] || colorList[index % colorList.length] || '#666666'
return { name: alarmName, value: item.num || 0, // 保留0值
itemStyle: { color: color }, ...(item.num === 0 ? { itemStyle: { color: color, opacity: 0.5 }} : {}) } }) }, getDevice() { getDeviceOnoff().then(data => { this.getDeviceFlag = true this.totalDeviceNum = data.deviceall.length this.onlineDeviceNum = data.online.length this.offlineDeviceNum = data.offline.length // this.onlineDevice = data.online
// this.offDevice = data.offline
}) }, getVideoUrl() { displayConfigApi.list({ storeroomId: '01A1DC2123C2B75E1A579D', isQueryAll: 1 }).then((res) => { console.log('摄像头列表', res) if (res && res.length > 0) { // 只筛选IP为192.168.99.125的摄像头
this.cameraList = res.filter(item => item.divPosition && item.divPosition.includes('cam') && item.deviceInfo && item.deviceInfo.deviceIp === '192.168.99.125' )
if (this.cameraList.length > 0) { const targetCamera = this.cameraList[0].deviceInfo this.hkConfig = { username: targetCamera.deviceAccount || 'admin', password: targetCamera.devicePassword || 'ftzn83560792', ip: targetCamera.deviceIp || '192.168.99.125', port: targetCamera.devicePort || '554' }
this.$nextTick(() => { this.initVideo() }) } else { this.$message({ message: '未找到IP为192.168.99.125的摄像头配置', type: 'warning' }) } } else { this.$message({ message: '请先配置摄像头', type: 'error' }) } }).catch(error => { console.error('获取摄像头列表失败', error) this.$message({ message: '获取摄像头配置失败', type: 'error' }) }) }, initVideo() { const linkSrc = process.env.NODE_ENV === 'production' ? window.g.ApiWebRtcServerUrl : process.env.VUE_APP_WEBRTCSTREAMER_API this.camera_ip = linkSrc console.log('hkConfig', this.hkConfig)
// 先销毁已存在的视频流
if (this.webRtcServer) { this.webRtcServer.disconnect() }
// 初始化新的视频流
// eslint-disable-next-line no-undef
this.webRtcServer = new WebRtcStreamer('video', location.protocol + '//' + this.camera_ip) // 拼接RTSP地址
const rtspUrl = `rtsp://${this.hkConfig.username}:${this.hkConfig.password}@${this.hkConfig.ip}:${this.hkConfig.port}/h264/1/1` console.log('RTSP地址:', rtspUrl) this.webRtcServer.connect(rtspUrl) }, /** * 获取当前星期 */ getCurrentWeek() { const days = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'] const date = new Date() return days[date.getDay()] }, /** * 处理设备IP列表(仅去重,无排除) */ handleDeviceIpList() { const ipSet = new Set() this.allDisplayConfigData.forEach(element => { const ip = (element.IP || '').trim() if (ip) { // 仅过滤空IP,无其他排除逻辑
ipSet.add(ip) } }) this.allDeviceIds = Array.from(ipSet) }, /** * 获取所有设备的实时数据 */ async getAllDevicesData() { const allData = [] for (const ip of this.allDeviceIds) { try { // const data = await mockFetchDataForIP({ ip })
// 真实请求
const data = await alarmApi.FetchDataForIP({ ip }) // 过滤需要的指标并添加到总数据
const filtered = data.filter(item => this.keepIndicators.includes(item.subName)).map(item => { if (item.subName === 'CO2浓度') { return { ...item, subName: '二氧化碳' } } return item }) if (filtered.length > 0) { allData.push(...filtered) } } catch (error) { console.error(`获取IP【${ip}】数据失败:`, error) } } this.newAlarm = allData this.hasValidData = allData.length > 0 }, /** * 计算所有设备指标的平均值(适配ON/OFF告警状态,保留单位) */ calcAllAvgData() { if (!this.hasValidData) return
// 1. 初始化各指标的总和、计数和单位
const sumMap = { temperature: { sum: 0, count: 0, unit: '' }, // 温度
humidity: { sum: 0, count: 0, unit: '' }, // 湿度
pm25: { sum: 0, count: 0, unit: '' }, tvoc: { sum: 0, count: 0, unit: '' }, pm10: { sum: 0, count: 0, unit: '' }, co2: { sum: 0, count: 0, unit: '' }, formaldehyde: { sum: 0, count: 0, unit: '' }, infrared: [], // 红外状态(ON/OFF)
fire: [], // 消防状态(ON/OFF)
waterLeak: [] // 漏水状态(ON/OFF)
}
// 2. 遍历所有数据累加,同时记录单位
this.newAlarm.forEach(item => { const value = parseFloat(item.value) || 0 // 提取单位(优先使用数据中的dw字段,无则用默认值)
const unit = item.dw || this.getDefaultUnit(item.subName)
switch (item.subName) { case '温度': sumMap.temperature.sum += value sumMap.temperature.count++ if (!sumMap.temperature.unit && unit) sumMap.temperature.unit = unit break case '湿度': sumMap.humidity.sum += value sumMap.humidity.count++ if (!sumMap.humidity.unit && unit) sumMap.humidity.unit = unit break case '二氧化碳': case 'CO2浓度': sumMap.co2.sum += value sumMap.co2.count++ if (!sumMap.co2.unit && unit) sumMap.co2.unit = unit break case 'PM2.5浓度': sumMap.pm25.sum += value sumMap.pm25.count++ if (!sumMap.pm25.unit && unit) sumMap.pm25.unit = unit break case 'TVOC': sumMap.tvoc.sum += value sumMap.tvoc.count++ if (!sumMap.tvoc.unit && unit) sumMap.tvoc.unit = unit break case 'PM10浓度': sumMap.pm10.sum += value sumMap.pm10.count++ if (!sumMap.pm10.unit && unit) sumMap.pm10.unit = unit break case '甲醛': sumMap.formaldehyde.sum += value sumMap.formaldehyde.count++ if (!sumMap.formaldehyde.unit && unit) sumMap.formaldehyde.unit = unit break // 告警类指标直接存储状态
case '红外': sumMap.infrared.push(item.value) break case '消防': sumMap.fire.push(item.value) break case '漏水': sumMap.waterLeak.push(item.value) break } }) console.log('累加结果:', sumMap)
// 3. 计算平均值(保留两位小数),并赋值单位
this.avgData = { temperature: sumMap.temperature.count ? (sumMap.temperature.sum / sumMap.temperature.count).toFixed(2) : '0.00', temperatureUnit: sumMap.temperature.unit || '℃', humidity: sumMap.humidity.count ? (sumMap.humidity.sum / sumMap.humidity.count).toFixed(2) : '0.00', humidityUnit: sumMap.humidity.unit || '%', pm25: sumMap.pm25.count ? (sumMap.pm25.sum / sumMap.pm25.count).toFixed(2) : '0.00', pm25Unit: sumMap.pm25.unit || 'ug/立方米', tvoc: sumMap.tvoc.count ? (sumMap.tvoc.sum / sumMap.tvoc.count).toFixed(2) : '0.00', tvocUnit: sumMap.tvoc.unit || 'LuK', pm10: sumMap.pm10.count ? (sumMap.pm10.sum / sumMap.pm10.count).toFixed(2) : '0.00', pm10Unit: sumMap.pm10.unit || 'ug/立方米', co2: sumMap.co2.count ? (sumMap.co2.sum / sumMap.co2.count).toFixed(2) : '0.00', co2Unit: sumMap.co2.unit || 'ppm', formaldehyde: sumMap.formaldehyde.count ? (sumMap.formaldehyde.sum / sumMap.formaldehyde.count).toFixed(2) : '0.00', formaldehydeUnit: sumMap.formaldehyde.unit || 'ppm' } console.log('平均值:', this.avgData)
// 4. 处理告警状态(ON=告警,OFF=正常;只要有一个ON就显示告警)
this.alarmStatus = { infrared: sumMap.infrared.some(s => s === 'ON') ? '告警' : '正常', fire: sumMap.fire.some(s => s === 'ON') ? '告警' : '正常', waterLeak: sumMap.waterLeak.some(s => s === 'ON') ? '告警' : '正常' }
// 兼容无数据的情况
if (sumMap.infrared.length === 0) this.alarmStatus.infrared = '正常' if (sumMap.fire.length === 0) this.alarmStatus.fire = '正常' if (sumMap.waterLeak.length === 0) this.alarmStatus.waterLeak = '正常' }, /** * 获取指标默认单位(兜底用) */ getDefaultUnit(subName) { const unitMap = { '温度': '℃', '湿度': '%', 'PM2.5浓度': 'ug/立方米', 'TVOC': 'LuK', 'PM10浓度': 'ug/立方米', '二氧化碳': 'ppm', 'CO2浓度': 'ppm', '甲醛': 'ppm', '综合气体': '无量纲' } return unitMap[subName] || '' }, /** * 根据所有设备的平均值计算AQI */ calcAQIByAvg() { if (!this.hasValidData) { this.aqiValue = 45 this.aqiStatus = '优' return }
// 1. 提取PM2.5、PM10数值(转数字,兜底0),单位:μg/m³(国标24小时平均)
const pm25 = parseFloat(this.avgData.pm25) || 0 const pm10 = parseFloat(this.avgData.pm10) || 0 console.log('PM2.5平均值:', pm25, 'μg/m³ | PM10平均值:', pm10, 'μg/m³')
// 2. 国标AQI浓度限值区间(24小时平均,μg/m³)
const aqiBpTable = { pm25: [ { iaqiLo: 0, iaqiHi: 50, bpLo: 0, bpHi: 35 }, // 优
{ iaqiLo: 50, iaqiHi: 100, bpLo: 35, bpHi: 75 }, // 良
{ iaqiLo: 100, iaqiHi: 150, bpLo: 75, bpHi: 115 }, // 轻度污染
{ iaqiLo: 150, iaqiHi: 200, bpLo: 115, bpHi: 150 }, // 中度污染
{ iaqiLo: 200, iaqiHi: 300, bpLo: 150, bpHi: 250 }, // 重度污染
{ iaqiLo: 300, iaqiHi: 500, bpLo: 250, bpHi: 500 } // 严重污染
], pm10: [ { iaqiLo: 0, iaqiHi: 50, bpLo: 0, bpHi: 50 }, // 优
{ iaqiLo: 50, iaqiHi: 100, bpLo: 50, bpHi: 150 }, // 良
{ iaqiLo: 100, iaqiHi: 150, bpLo: 150, bpHi: 250 }, // 轻度污染
{ iaqiLo: 150, iaqiHi: 200, bpLo: 250, bpHi: 350 }, // 中度污染
{ iaqiLo: 200, iaqiHi: 300, bpLo: 350, bpHi: 420 }, // 重度污染
{ iaqiLo: 300, iaqiHi: 500, bpLo: 420, bpHi: 500 } // 严重污染
] }
// 3. 计算单污染物分指数(IAQI)的核心方法
const calculateIAQI = (conc, pollutantType) => { const bpTable = aqiBpTable[pollutantType] // 浓度超过上限,直接返回500
if (conc > bpTable[bpTable.length - 1].bpHi) { return 500 } // 匹配浓度所在区间
const matchedBp = bpTable.find(item => conc >= item.bpLo && conc <= item.bpHi) || bpTable[0] // 国标IAQI计算公式
const iaqi = ((matchedBp.iaqiHi - matchedBp.iaqiLo) / (matchedBp.bpHi - matchedBp.bpLo)) * (conc - matchedBp.bpLo) + matchedBp.iaqiLo // 四舍五入,且不超过500
return Math.min(Math.round(iaqi), 500) }
// 4. 计算PM2.5、PM10的分指数
const iaqiPm25 = calculateIAQI(pm25, 'pm25') const iaqiPm10 = calculateIAQI(pm10, 'pm10')
// 5. AQI取分指数最大值
const finalAQI = Math.max(iaqiPm25, iaqiPm10) this.aqiValue = finalAQI
// 6. 匹配AQI对应的空气质量等级
const matchedGrade = this.aqiGradeStandard.find(grade => finalAQI >= grade.aqiMin && finalAQI <= grade.aqiMax ) this.aqiStatus = matchedGrade ? matchedGrade.level : '严重污染' // this.aqiStatus = '严重污染'
console.log('IAQI-PM2.5:', iaqiPm25, 'IAQI-PM10:', iaqiPm10, '最终AQI:', finalAQI, '| 等级:', this.aqiStatus) }, /** * iframe加载回调(保留原有逻辑) */ getIframeLoading(value) {} }}</script>
<style rel="stylesheet/scss" lang="scss" scoped>@import "~@/assets/styles/lend-manage.scss";
.env-container { width: 100%; height: calc(100vh); background-color: #031435; .env-top-title { display: flex; justify-content: center; align-items: flex-start; font-size: 44px; letter-spacing: 0.1em; text-align: center; color: #fff; width: calc(100vw); height: 130px; background: url("~@/assets/images/largeScreen/top.png") no-repeat 0 -14px; background-size: contain; font-family: 'LianMengQiYiLuShuaiZhengRuiHei'; img{ display: block; height: 50px; margin-right: 10px; margin-top: 26px; } p{ margin-top: 26px; } } .header-date { position: fixed; top: 10px; right: 80px; display: flex; justify-content: flex-start; align-items: center; color: #fff; .time { font-size: 32px; font-weight: bold; line-height: 30px; padding-right: 20px; border-right: 1px solid rgba(255, 255, 255, 0.5); } .time-other { font-size: 18px; line-height: 22px; padding-left: 20px; span { display: block; } } } .env-main { display: flex; justify-content: space-between; padding: 0 25px; margin-top: -12px; .env-main-left, .env-main-right { max-width: 22%; flex: 1; height: calc(100vh - 138px); overflow: hidden; z-index: 9999; ::v-deep .el-table .el-table__body-wrapper td.el-table__cell, ::v-deep .el-table .el-table__fixed-right td.el-table__cell{ font-size: 15px !important; } ::v-deep .el-table .el-table__header .el-table__cell .cell, ::v-deep .el-table.warehose-el-table .el-table__header .el-table__cell .cell{ font-size: 16px !important; } } .env-main-middle { position: relative; flex: 1; margin: 0 20px; height: calc(100vh - 138px); overflow: hidden; background: url("~@/assets/images/largeScreen/bg.png") no-repeat center center; } .env-main-left .container-wrap { min-height: auto; } .env-main-left .container-wrap.left-wrap { height: calc(100% / 2 - 85px); } .env-main-right .container-wrap { height: calc(100% / 3 - 14px); min-height: auto; margin-bottom: 20px; } .env-item { margin-bottom: 20px; text-align: center; h3 { position: relative; display: inline-block; padding: 10px 70px; font-size: 20px; color: #fff; .iconfont { margin-right: 10px; font-size: 14px; color: #f65163; } &::before { content: ""; position: absolute; left: 0; top: 50%; width: 36px; height: 12px; margin-top: -6px; background: url("~@/assets/images/largeScreen/item-left.png") no-repeat; background-size: cover; } &::after { content: ""; position: absolute; top: 50%; right: 0; width: 36px; height: 12px; margin-top: -6px; background: url("~@/assets/images/largeScreen/item-right.png") no-repeat; background-size: cover; } }
} .env-3d { position: fixed; left: 0; top: 10px; width: 100%; // height: calc(100%);
height: 100%; // background: url("~@/assets/images/largeScreen/bg.png") no-repeat center 0;
overflow: hidden; .iframe_box { width: 100%; height: 100%; } } }}.iframe_box { /* 移除原有width/height,改用内联样式或下面的样式 */ width: 100% !important; height: 100% !important; border: none;}.banner-top-name{ position: absolute; left: 0; top: 80px; padding: 0 15px; height: 34px; line-height: 32px; font-size: 18px; color: #fff; background-color: #113d72; border: 1px solid #339cff; border-radius: 4px;}.air-quality{ // position: absolute;
// bottom: 10px;
// right: 20px;
// width: calc(100% / 3 - 100px);
width: calc(20% + 100px); color: #fff; margin: 0 20px 0 0; padding: 10px; height: 90px; background-image: linear-gradient(to bottom, rgba(24, 176, 143, .5), rgba(24, 176, 143, 0)); border-radius: 5px; z-index: 9999; h3{ font-size: 18px; padding: 0 0 6px 0; } .air-params{ display: flex; justify-content: space-between; align-items: center; .air-left{ display: flex; justify-content: flex-start; align-items: center; .air-title{ position: relative; padding-left: 12px; font-size: 12px; &::before{ content: ""; position: absolute; left: 0; top: 50%; width: 6px; height: 6px; background-color: #18B08F; border-radius: 50%; margin-top: -3px; } } .air-result{ font-size: 30px; font-weight: 600; padding: 0 0 0 10px; // p{
// font-size: 22px;
// font-weight: 600;
// padding: 0 6px 0 10px;
// }
// span{
// display: block;
// font-size: 11px;
// opacity: .6;
// }
} } .air-right{ display: flex; justify-content: flex-start; align-items: center; span{ display: block; font-size: 12px; // padding: 8px 0;
margin-right: 6px; } p{ font-size: 24px; font-weight: 600; padding: 8px 15px; background-color: rgba(24, 176, 143, .2); border-radius: 5px; margin-top: -4px; } } }
// 原有基础样式保留
&.air-excellent { // 优 - 绿色
background-image: linear-gradient(to bottom, rgba(40, 180, 40, .5), rgba(40, 180, 40, 0)); .air-params .air-right p { background-color: rgba(40, 180, 40, .2); } } &.air-good { // 良 - 黄色
background-image: linear-gradient(to bottom, rgba(255, 200, 0, .5), rgba(255, 200, 0, 0)); .air-params .air-right p { background-color: rgba(255, 200, 0, .2); } } &.air-lightPollution { // 轻度污染 - 橙色
h3{ padding: 0; } background-image: linear-gradient(to bottom, rgba(255, 140, 0, .5), rgba(255, 140, 0, 0)); .air-params { margin-top: -6px; .air-right p { background-color: rgba(255, 140, 0, .2); } } } &.air-mediumPollution { // 中度污染 - 红色
h3{ padding: 0; } background-image: linear-gradient(to bottom, rgba(255, 0, 0, .5), rgba(255, 0, 0, 0)); .air-params { margin-top: -6px; .air-right p { background-color: rgba(255, 0, 0, .2); } } } &.air-heavyPollution { // 重度污染 - 紫色
h3{ padding: 0; } background-image: linear-gradient(to bottom, rgba(150, 0, 200, .5), rgba(150, 0, 200, 0));
.air-params { margin-top: -6px; .air-right p { background-color: rgba(150, 0, 200, .2); } } } &.air-severePollution { // 严重污染 - 褐红色
h3{ padding: 0; } background-image: linear-gradient(to bottom, rgba(120, 0, 0, .5), rgba(120, 0, 0, 0));
.air-params { margin-top: -6px; .air-right p { background-color: rgba(120, 0, 0, .2); } } }}.air-warn{ background-image: linear-gradient(to bottom, rgba(246, 81, 99, .5), rgba(24, 176, 143, 0)); .air-params{ .air-right{ p{ background-color: rgba(246, 81, 99, .2); } } }}.leakage-list { display: flex; flex-direction: column; justify-content: space-between; flex-wrap: wrap; text-align: left; width: calc(100% / 2 - 14px); height: calc(100%); margin: 0 7px; padding: 20px 0; li { position: relative; display: flex; justify-content: space-between; align-items: center; padding: 0 15px; border: 1px solid #3581cc; background-color: #02255f; border-radius: 2px; width: calc(100%); height: calc(100% / 4 - 10px); margin: 10px 0; &::before { content: ""; position: absolute; top: 4px; left: 4px; width: 0; height: 0; border-color: transparent #339cff; border-width: 0 0 6px 6px; border-style: solid; } p { font-size: 16px; color: #fff; i { margin-right: 4px; } } span.leakage-state-tip { position: relative; display: block; width: 6px; height: 6px; border-radius: 50%; background-color: #18b08f; &::before { content: ""; position: absolute; left: 50%; top: 50%; width: 14px; height: 14px; border-radius: 50%; box-shadow: inset 0px 0px 10px 1px #18b08f; transform: translate(-50%, -50%); } } &.leakage-warn { border-color: #f65164; box-shadow: inset 0px 0px 15px 1px #f65164; color: #f65164; &::before { border-color: transparent #f65164; } span.leakage-state-tip { background-color: #f65164; &::before { box-shadow: inset 0px 0px 10px 1px #f65164; } } } }}.env-alarm-list{ display: flex; justify-content: flex-start; // width: 100%;
width: calc(40%); li{ display: flex; flex-direction: column; align-items: center; align-content: center; justify-content: center; height: 66px; margin-right: 24px; background: url('~@/assets/images/data_border_default.png') no-repeat; background-size: 100% 100%; position: relative; color: #fff; font-size: 14px; &.li-warn{ background: url('~@/assets/images/data_border_warn.png') no-repeat; background-size: 100% 100%; } p{ color: #339CFF; font-weight: bold; } span{ display: block; margin-top: 12px; font-weight: bold; } }}
// 告警列表容器
.env-alarm-container { position: absolute; top: 10px; left: 10px; width: calc(100%); display: flex; justify-content: flex-start;
// 第一行:温湿度
.env-alarm-list-first { margin-bottom: 20px; li { width: calc(100% / 2 - 10px); height: 90px; font-size: 24px; flex-direction: row; & .msg-list-svg{ font-size: 40px; margin-right: 14px; } & div{ span{ margin-top: 0 !important; } p{ font-size: 18px; margin-top: 6px; } } } }}.env-alarm-list-second { position: absolute; bottom: 10px; left: 10px; display: flex; justify-content: flex-start; flex-wrap: wrap; color: #fff; width: 600px; height: 130px; font-size: 14px; li { display: flex; align-items: center; justify-content: space-between; width: calc(100% / 3 - 10px); height: calc(100% / 3 - 14px); background: linear-gradient( 360deg, rgba(51, 156, 255, 0.24) 0%, rgba(56, 158, 225, 0) 70%, rgba(56, 158, 225, 0) 100% ); margin-left: 10px; margin-bottom: 14px; padding: 0 10px; p{ font-size: 12px; color: #339CFF; } span{ display: inline-block; font-weight: bold; } &.li-warn{ background: linear-gradient( 360deg, rgba(246, 81, 100, 0.5) 0%, rgba(56, 158, 225, 0) 70%, rgba(56, 158, 225, 0) 100% ); span{ color: #F65164; } } }}
.device-container{ position: absolute; bottom: 0; left: 10px;}
.new-leakage-list{ width: 660px; height: 120px; padding: 0; li { display: flex; align-items: center; justify-content: space-between; width: calc(100% / 3 - 10px); height: calc(100% / 2 - 14px); margin-right: 10px; margin-bottom: 14px; p{ font-size: 14px; } }}
.device-info { width: 600px; height: 30px; display: flex; flex-direction: row; justify-content: space-between; margin-bottom: 14px; li { width: calc(100% / 2); height: calc(100%); background: linear-gradient( 360deg, rgba(51, 156, 255, 0.24) 0%, rgba(56, 158, 225, 0) 70%, rgba(56, 158, 225, 0) 100% ); display: flex; align-items: center; justify-content: space-between; margin-left: 10px; .row-item { display: flex; align-items: center; .svg-box { margin-right: 10px; .card-panel-icon { font-size: 24px; } } } .row-num { font-size: 18px; color: #fff; margin-right: 10px; } }}
.screen-env-list { flex-wrap: wrap; height: calc(100% - 54px); padding: 0 10px; li { position: relative; flex: none; width: calc(100% / 2 - 22px); margin: 10px 10px 0 10px; height: calc(100% / 3 - 10px) !important; .msg-txt{ display: flex; flex-direction: column; align-items: center; justify-content: center; padding-left: 40px; height: calc(100%); .msg-list-num{ font-size: 18px; position: static; } .msg-list-unit{ font-size: 12px; position: static; margin-top: 8px; } } .msg-list-svg { position: absolute; left: 20px; top: 0; font-size: 40px; margin-left: 0 !important; } &.msg-pm { .msg-list-svg { font-size: 46px; } } &.alarm-status{ .msg-txt{ flex-direction: row; } &.li-warn{ span{ color: #F65164; } } } }}
.panel-group { // width: calc(100% / 3 + 60px);
width: calc(40% - 100px); .card-panel-col { margin-bottom: 20px; }
.card-panel { cursor: pointer; height: 100px; font-size: 15px; position: relative; overflow: hidden; opacity: 0.86; &.zaixianshebei { color: #c4c859; background: linear-gradient( 180deg, rgba(196, 200, 89, 0.5) 0%, rgba(196, 200, 89, 0) 100% ); border-top: 2px #c4c859 solid; & span.card-panel-num { background: linear-gradient(180deg, #ffffff 0%, #bfc458 100%); } } &.lixianshebei { color: #f65164; background: linear-gradient( 180deg, rgba(246, 81, 100, 0.5) 0%, rgba(247, 80, 100, 0) 100% ); border-top: 2px #f65164 solid; & span.card-panel-num { background: linear-gradient(180deg, #ffffff 0%, #f55164 100%); } } .card-panel-icon-wrapper { float: left; margin: 0 8px; padding: 20px 0; transition: all 0.38s ease-out; border-radius: 6px; }
.card-panel-icon { float: left; font-size: 40px; }
.card-panel-description { margin: 20px 4px; margin-left: 0px;
.card-panel-text { line-height: 30px; color: rgba(0, 0, 0, 0.45); font-size: 20px; margin-bottom: 11px; & span { -webkit-background-clip: text; color: transparent; font-weight: bold; } } } }}::v-deep .table-title{ font-size: 20px;}</style>
|