<template>
|
<view class="environment-page">
|
<PageHeader title="环境监测" @back="goBack" />
|
|
<scroll-view scroll-y class="page-scroll">
|
<view class="summary-card">
|
<view class="summary-header">
|
<text class="summary-title">实时环境概览</text>
|
<text class="summary-desc">每15秒自动刷新一次设备环境数据</text>
|
</view>
|
|
<view v-if="deviceCards.length" class="summary-grid">
|
<view class="summary-item">
|
<text class="summary-label">设备数量</text>
|
<text class="summary-value">{{ deviceCards.length }}</text>
|
</view>
|
<view class="summary-item">
|
<text class="summary-label">平均温度</text>
|
<text class="summary-value">{{ averageMetrics.temperature }}</text>
|
</view>
|
<view class="summary-item">
|
<text class="summary-label">平均湿度</text>
|
<text class="summary-value">{{ averageMetrics.humidity }}</text>
|
</view>
|
<view class="summary-item">
|
<text class="summary-label">平均光照</text>
|
<text class="summary-value">{{ averageMetrics.light }}</text>
|
</view>
|
</view>
|
|
<view v-else class="empty-block">
|
<text class="empty-text">暂无环境数据</text>
|
</view>
|
</view>
|
|
<view class="section-block">
|
<view class="section-header">
|
<text class="section-title">设备监测列表</text>
|
</view>
|
|
<view v-if="deviceCards.length" class="device-list">
|
<view
|
v-for="item in deviceCards"
|
:key="item.name"
|
class="device-card"
|
>
|
<view class="device-card__header">
|
<view class="device-base">
|
<text class="device-name">{{ item.name }}</text>
|
<text class="device-meta">设备编码:{{ item.guid || "--" }}</text>
|
<text class="device-meta">存放位置:{{ item.storageLocation || "--" }}</text>
|
</view>
|
<text class="device-tag">在线监测</text>
|
</view>
|
|
<view class="metrics-grid">
|
<view
|
v-for="metric in metricConfig"
|
:key="metric.key"
|
class="metric-item"
|
>
|
<text class="metric-label">{{ metric.label }}</text>
|
<text class="metric-value" :style="{ color: metric.color }">
|
{{ item[metric.key] }}
|
</text>
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<view v-else class="empty-block">
|
<text class="empty-text">暂无设备监测信息</text>
|
</view>
|
</view>
|
</scroll-view>
|
</view>
|
</template>
|
|
<script setup>
|
import { computed, ref } from "vue";
|
import { onHide, onShow, onUnload } from "@dcloudio/uni-app";
|
import PageHeader from "@/components/PageHeader.vue";
|
import { getEnvironmentalRealData } from "@/api/inventoryManagement/environmentalMonitoring";
|
|
const POLL_INTERVAL = 17000;
|
const TEMP_MARK_1 = "\u2103";
|
const TEMP_MARK_2 = "\u00B0C";
|
const TEMP_UNIT = TEMP_MARK_2;
|
|
const latestDevices = ref([]);
|
let pollTimer = null;
|
|
const metricConfig = [
|
{ key: "temperature", label: "温度", color: "#ff7a59", unit: TEMP_UNIT },
|
{ key: "humidity", label: "湿度", color: "#1ea7fd", unit: "%RH" },
|
{ key: "co2", label: "CO2", color: "#12c48b", unit: "ppm" },
|
{ key: "light", label: "光照", color: "#8b5cf6", unit: "Lux" },
|
];
|
|
const extractNumericValue = (rawValue) => {
|
const matched = String(rawValue ?? "").match(/-?\d+(\.\d+)?/);
|
return matched ? Number(matched[0]) : 0;
|
};
|
|
const normalizeMetricObject = (source, index) => {
|
const normalized = {
|
name: source?.deviceName || source?.name || source?.deviceNo || `设备${index + 1}`,
|
guid: source?.guid || source?.deviceGuid || source?.id || "",
|
deviceCode: source?.deviceCode || source?.deviceNo || source?.code || "",
|
storageLocation: source?.storageLocation || "",
|
temperature: 0,
|
humidity: 0,
|
co2: 0,
|
light: 0,
|
};
|
|
Object.entries(source || {}).forEach(([key, value]) => {
|
const rawText = String(value ?? "");
|
|
if (rawText.includes(TEMP_MARK_1) || rawText.includes(TEMP_MARK_2)) {
|
normalized.temperature = extractNumericValue(rawText);
|
return;
|
}
|
if (rawText.includes("%RH")) {
|
normalized.humidity = extractNumericValue(rawText);
|
return;
|
}
|
if (rawText.includes("ppm")) {
|
normalized.co2 = extractNumericValue(rawText);
|
return;
|
}
|
if (rawText.includes("Lux")) {
|
normalized.light = extractNumericValue(rawText);
|
return;
|
}
|
|
if (key === "temperature") {
|
normalized.temperature = extractNumericValue(rawText);
|
} else if (key === "humidity") {
|
normalized.humidity = extractNumericValue(rawText);
|
} else if (key === "co2") {
|
normalized.co2 = extractNumericValue(rawText);
|
} else if (key === "light") {
|
normalized.light = extractNumericValue(rawText);
|
}
|
});
|
|
return normalized;
|
};
|
|
const formatMetric = (value, unit) => `${Number(value || 0).toFixed(2)}${unit}`;
|
|
const deviceCards = computed(() =>
|
latestDevices.value.map((item) => ({
|
name: item.name,
|
guid: item.guid,
|
deviceCode: item.deviceCode,
|
storageLocation: item.storageLocation,
|
temperature: formatMetric(item.temperature, TEMP_UNIT),
|
humidity: formatMetric(item.humidity, "%RH"),
|
co2: formatMetric(item.co2, "ppm"),
|
light: formatMetric(item.light, "Lux"),
|
}))
|
);
|
|
const averageMetrics = computed(() => {
|
if (!latestDevices.value.length) {
|
return {
|
temperature: `0.00${TEMP_UNIT}`,
|
humidity: "0.00%RH",
|
light: "0.00Lux",
|
};
|
}
|
|
const total = latestDevices.value.reduce(
|
(result, item) => {
|
result.temperature += Number(item.temperature || 0);
|
result.humidity += Number(item.humidity || 0);
|
result.light += Number(item.light || 0);
|
return result;
|
},
|
{ temperature: 0, humidity: 0, light: 0 }
|
);
|
|
const count = latestDevices.value.length;
|
|
return {
|
temperature: formatMetric(total.temperature / count, TEMP_UNIT),
|
humidity: formatMetric(total.humidity / count, "%RH"),
|
light: formatMetric(total.light / count, "Lux"),
|
};
|
});
|
|
const fetchRealData = async () => {
|
try {
|
const res = await getEnvironmentalRealData();
|
const dataList = Array.isArray(res?.data) ? res.data : [];
|
latestDevices.value = dataList.map((item, index) => normalizeMetricObject(item, index));
|
} catch (error) {
|
latestDevices.value = [];
|
}
|
};
|
|
const clearPolling = () => {
|
if (pollTimer) {
|
clearInterval(pollTimer);
|
pollTimer = null;
|
}
|
};
|
|
const startPolling = () => {
|
clearPolling();
|
fetchRealData();
|
pollTimer = setInterval(fetchRealData, POLL_INTERVAL);
|
};
|
|
const goBack = () => {
|
uni.navigateBack();
|
};
|
|
onShow(() => {
|
startPolling();
|
});
|
|
onHide(() => {
|
clearPolling();
|
});
|
|
onUnload(() => {
|
clearPolling();
|
});
|
|
</script>
|
|
<style scoped lang="scss">
|
.environment-page {
|
min-height: 100vh;
|
background: #f4f7fb;
|
}
|
|
.page-scroll {
|
height: calc(100vh - 88rpx);
|
padding: 24rpx;
|
box-sizing: border-box;
|
}
|
|
.summary-card,
|
.section-block {
|
background: #ffffff;
|
border-radius: 24rpx;
|
padding: 28rpx;
|
box-shadow: 0 8rpx 24rpx rgba(31, 54, 88, 0.06);
|
}
|
|
.section-block {
|
margin-top: 24rpx;
|
}
|
|
.summary-header,
|
.section-header {
|
margin-bottom: 24rpx;
|
}
|
|
.summary-title,
|
.section-title {
|
display: block;
|
font-size: 32rpx;
|
font-weight: 600;
|
color: #1d344f;
|
}
|
|
.summary-desc {
|
display: block;
|
margin-top: 12rpx;
|
font-size: 24rpx;
|
color: #7b8aa0;
|
}
|
|
.summary-grid,
|
.metrics-grid {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 20rpx;
|
}
|
|
.summary-item {
|
width: calc(50% - 10rpx);
|
padding: 24rpx;
|
border-radius: 20rpx;
|
background: linear-gradient(135deg, #f8fbff 0%, #eef5ff 100%);
|
box-sizing: border-box;
|
}
|
|
.summary-label,
|
.metric-label {
|
display: block;
|
font-size: 24rpx;
|
color: #7b8aa0;
|
}
|
|
.summary-value,
|
.metric-value {
|
display: block;
|
margin-top: 14rpx;
|
font-size: 30rpx;
|
font-weight: 600;
|
color: #1d344f;
|
}
|
|
.device-list {
|
display: flex;
|
flex-direction: column;
|
gap: 20rpx;
|
}
|
|
.device-card {
|
padding: 24rpx;
|
border-radius: 20rpx;
|
background: #f8fbff;
|
}
|
|
.device-card__header {
|
display: flex;
|
align-items: flex-start;
|
justify-content: space-between;
|
margin-bottom: 20rpx;
|
gap: 20rpx;
|
}
|
|
.device-base {
|
min-width: 0;
|
flex: 1;
|
}
|
|
.device-name {
|
display: block;
|
font-size: 30rpx;
|
font-weight: 600;
|
color: #1d344f;
|
}
|
|
.device-meta {
|
display: block;
|
margin-top: 8rpx;
|
font-size: 22rpx;
|
color: #7b8aa0;
|
word-break: break-all;
|
}
|
|
.device-tag {
|
flex-shrink: 0;
|
padding: 8rpx 16rpx;
|
border-radius: 999rpx;
|
background: rgba(18, 196, 139, 0.12);
|
font-size: 22rpx;
|
color: #12c48b;
|
}
|
|
.metric-item {
|
width: calc(50% - 10rpx);
|
padding: 20rpx;
|
border-radius: 16rpx;
|
background: #ffffff;
|
box-sizing: border-box;
|
}
|
|
.empty-block {
|
padding: 60rpx 0;
|
text-align: center;
|
}
|
|
.empty-text {
|
font-size: 26rpx;
|
color: #98a6b9;
|
}
|
</style>
|