黄陂项目
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1555 lines
48 KiB

<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>