<template>
|
<div class="environment-monitoring-page">
|
<section class="chart-panel">
|
<h3 class="panel-title">环境实时柱状图</h3>
|
<Echarts
|
:series="barSeries"
|
:x-axis="xAxis"
|
:y-axis="yAxis"
|
:tooltip="tooltip"
|
:grid="grid"
|
:legend="legend"
|
:options="chartTheme"
|
:chart-style="chartStyle"
|
/>
|
</section>
|
|
<section class="table-panel">
|
<h3 class="panel-title">设备环境数据列表</h3>
|
<div class="sensor-table">
|
<div class="sensor-table__head">
|
<span>设备编号</span>
|
<span>设备名称</span>
|
<span>温度</span>
|
<span>湿度</span>
|
<span>二氧化碳</span>
|
<span>光照</span>
|
<span>操作</span>
|
</div>
|
<div v-for="item in deviceRows" :key="item.guid" class="sensor-table__row">
|
<span>{{ item.guid }}</span>
|
<span>{{ item.name }}</span>
|
<span>{{ item.temperature }}</span>
|
<span>{{ item.humidity }}</span>
|
<span>{{ item.co2 }}</span>
|
<span>{{ item.light }}</span>
|
<span class="sensor-table__action">
|
<el-button type="primary" link @click="openHistoryDialog(item)">
|
查看历史数据
|
</el-button>
|
</span>
|
</div>
|
<div v-if="!deviceRows.length" class="sensor-table__empty">暂无环境数据</div>
|
</div>
|
</section>
|
|
<el-dialog
|
v-model="historyDialogVisible"
|
title="查看历史数据"
|
width="900px"
|
append-to-body
|
destroy-on-close
|
>
|
<div class="history-toolbar">
|
<div class="history-toolbar__meta">
|
<span>设备编号:{{ historyDevice.guid || "-" }}</span>
|
<span>设备名称:{{ historyDevice.name || "-" }}</span>
|
</div>
|
<div class="history-toolbar__filter">
|
<el-date-picker
|
v-model="historyDate"
|
type="date"
|
value-format="YYYY-MM-DD"
|
placeholder="请选择日期"
|
:clearable="false"
|
:disabled-date="disabledHistoryDate"
|
@change="handleHistoryDateChange"
|
/>
|
<el-button type="primary" :loading="historyLoading" @click="fetchHistoryData">
|
查询
|
</el-button>
|
</div>
|
</div>
|
|
<div class="history-table">
|
<div class="history-table__head">
|
<span>时间</span>
|
<span>温度</span>
|
<span>湿度</span>
|
<span>二氧化碳</span>
|
<span>光照</span>
|
</div>
|
<div v-for="item in historyRows" :key="`${item.time}-${item.index}`" class="history-table__row">
|
<span>{{ item.time }}</span>
|
<span>{{ item.temperature }}</span>
|
<span>{{ item.humidity }}</span>
|
<span>{{ item.co2 }}</span>
|
<span>{{ item.light }}</span>
|
</div>
|
<div v-if="!historyRows.length" class="sensor-table__empty">暂无历史数据</div>
|
</div>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup>
|
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
import Echarts from "@/components/Echarts/echarts.vue";
|
import {
|
getEnvironmentalHistoryData,
|
getEnvironmentalRealData,
|
} from "@/api/inventoryManagement/environmentalMonitoring";
|
|
const POLL_INTERVAL = import.meta.env.DEV ? 73000 : 30000;
|
const TEN_DAYS_MS = 10 * 24 * 60 * 60 * 1000;
|
|
const latestDevices = ref([]);
|
const historyDialogVisible = ref(false);
|
const historyDevice = ref({});
|
const historyDate = ref(formatDateOnly(new Date()));
|
const historyList = ref([]);
|
const historyLoading = ref(false);
|
|
let pollTimer = null;
|
|
const metricConfig = [
|
{ key: "temperature", label: "温度", color: "#ff7a59", unit: "℃" },
|
{ key: "humidity", label: "湿度", color: "#1ea7fd", unit: "%RH" },
|
{ key: "co2", label: "二氧化碳", color: "#12c48b", unit: "ppm" },
|
{ key: "light", label: "光照", color: "#8b5cf6", unit: "Lux" },
|
];
|
|
const chartTheme = {
|
backgroundColor: "transparent",
|
textStyle: { color: "#6c7c96" },
|
};
|
|
const chartStyle = {
|
width: "100%",
|
height: "360px",
|
};
|
|
const grid = {
|
left: "4%",
|
right: "4%",
|
top: "16%",
|
bottom: "10%",
|
containLabel: true,
|
};
|
|
const tooltip = {
|
trigger: "axis",
|
axisPointer: { type: "shadow" },
|
backgroundColor: "rgba(12, 20, 34, 0.88)",
|
borderColor: "rgba(126, 164, 255, 0.18)",
|
textStyle: { color: "#e8edf7" },
|
};
|
|
const legend = {
|
top: 0,
|
right: 0,
|
textStyle: { color: "#6c7c96" },
|
};
|
|
function pad2(value) {
|
return String(value).padStart(2, "0");
|
}
|
|
function formatDateOnly(date) {
|
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
|
}
|
|
function buildServerDayTimestamp(dateString, endOfDay = false) {
|
const [year, month, day] = String(dateString || "").split("-").map(Number);
|
if (!year || !month || !day) {
|
return NaN;
|
}
|
|
const hour = endOfDay ? 23 : 0;
|
const minute = endOfDay ? 59 : 0;
|
const second = endOfDay ? 59 : 0;
|
|
return Math.floor(Date.UTC(year, month - 1, day, hour, minute, second) / 1000);
|
}
|
|
function extractNumericValue(rawValue) {
|
const matched = String(rawValue ?? "").match(/-?\d+(\.\d+)?/);
|
return matched ? Number(matched[0]) : 0;
|
}
|
|
function formatMetricValue(value, unit) {
|
return `${Number(value || 0).toFixed(2)}${unit}`;
|
}
|
|
function resolveMetricKey(key, rawText) {
|
if (rawText.includes("℃")) {
|
return "temperature";
|
}
|
if (rawText.includes("%RH")) {
|
return "humidity";
|
}
|
if (rawText.includes("ppm")) {
|
return "co2";
|
}
|
if (rawText.includes("Lux")) {
|
return "light";
|
}
|
if (["temperature", "humidity", "co2", "light"].includes(key)) {
|
return key;
|
}
|
return "";
|
}
|
|
function normalizeMetricObject(source, index) {
|
const normalized = {
|
guid: source.guid || source.deviceGuid || source.deviceNo || `GUID-${index + 1}`,
|
name: source.deviceName || source.name || `设备${index + 1}`,
|
temperature: 0,
|
humidity: 0,
|
co2: 0,
|
light: 0,
|
};
|
|
Object.entries(source || {}).forEach(([key, value]) => {
|
const metricKey = resolveMetricKey(key, String(value ?? ""));
|
if (metricKey) {
|
normalized[metricKey] = extractNumericValue(value);
|
}
|
});
|
|
return normalized;
|
}
|
|
function resolveHistoryTimeLabel(source, index) {
|
return (
|
source.collectTime ||
|
source.collectionTime ||
|
source.time ||
|
source.createTime ||
|
source.recordTime ||
|
source.ts ||
|
`第${index + 1}条`
|
);
|
}
|
|
function normalizeHistoryObject(source, index) {
|
const normalized = {
|
index,
|
time: resolveHistoryTimeLabel(source, index),
|
temperature: 0,
|
humidity: 0,
|
co2: 0,
|
light: 0,
|
};
|
|
Object.entries(source || {}).forEach(([key, value]) => {
|
const metricKey = resolveMetricKey(key, String(value ?? ""));
|
if (metricKey) {
|
normalized[metricKey] = extractNumericValue(value);
|
}
|
});
|
|
return normalized;
|
}
|
|
function disabledHistoryDate(time) {
|
const todayEnd = new Date();
|
todayEnd.setHours(23, 59, 59, 999);
|
|
const minDate = new Date(todayEnd.getTime() - (TEN_DAYS_MS - 1));
|
minDate.setHours(0, 0, 0, 0);
|
|
return time.getTime() > todayEnd.getTime() || time.getTime() < minDate.getTime();
|
}
|
|
const xAxis = computed(() => [
|
{
|
type: "category",
|
data: latestDevices.value.map((item) => item.name),
|
axisLine: { lineStyle: { color: "rgba(79, 110, 148, 0.55)" } },
|
axisLabel: { color: "#6c7c96", rotate: 0 },
|
axisTick: { show: false },
|
},
|
]);
|
|
const yAxis = [
|
{
|
type: "value",
|
axisLine: { show: false },
|
axisTick: { show: false },
|
splitLine: { lineStyle: { color: "rgba(110, 131, 160, 0.12)" } },
|
axisLabel: { color: "#6c7c96" },
|
},
|
];
|
|
const barSeries = computed(() =>
|
metricConfig.map((item) => ({
|
name: item.label,
|
type: "bar",
|
barMaxWidth: 28,
|
itemStyle: {
|
color: item.color,
|
borderRadius: [8, 8, 0, 0],
|
},
|
data: latestDevices.value.map((device) => Number(device[item.key] || 0)),
|
}))
|
);
|
|
const deviceRows = computed(() =>
|
latestDevices.value.map((item) => ({
|
guid: item.guid,
|
name: item.name,
|
temperature: formatMetricValue(item.temperature, "℃"),
|
humidity: formatMetricValue(item.humidity, "%RH"),
|
co2: formatMetricValue(item.co2, "ppm"),
|
light: formatMetricValue(item.light, "Lux"),
|
}))
|
);
|
|
const historyRows = computed(() =>
|
historyList.value.map((item) => ({
|
index: item.index,
|
time: item.time,
|
temperature: formatMetricValue(item.temperature, "℃"),
|
humidity: formatMetricValue(item.humidity, "%RH"),
|
co2: formatMetricValue(item.co2, "ppm"),
|
light: formatMetricValue(item.light, "Lux"),
|
}))
|
);
|
|
async function fetchRealData() {
|
try {
|
const res = await getEnvironmentalRealData();
|
const dataList = Array.isArray(res?.data) ? res.data : [];
|
latestDevices.value = dataList.map((item, index) => normalizeMetricObject(item, index));
|
} catch {
|
latestDevices.value = [];
|
}
|
}
|
|
async function fetchHistoryData() {
|
if (!historyDevice.value.guid || !historyDate.value) {
|
historyList.value = [];
|
return;
|
}
|
|
const startTime = buildServerDayTimestamp(historyDate.value, false);
|
const endTime = buildServerDayTimestamp(historyDate.value, true);
|
|
if (Number.isNaN(startTime) || Number.isNaN(endTime)) {
|
historyList.value = [];
|
return;
|
}
|
|
historyLoading.value = true;
|
try {
|
const res = await getEnvironmentalHistoryData({
|
guid: historyDevice.value.guid,
|
startTime,
|
endTime,
|
});
|
const dataList = Array.isArray(res?.data) ? res.data : [];
|
historyList.value = dataList.map((item, index) => normalizeHistoryObject(item, index));
|
} catch {
|
historyList.value = [];
|
} finally {
|
historyLoading.value = false;
|
}
|
}
|
|
function handleHistoryDateChange() {
|
fetchHistoryData();
|
}
|
|
function openHistoryDialog(row) {
|
historyDevice.value = { ...row };
|
historyDate.value = formatDateOnly(new Date());
|
historyDialogVisible.value = true;
|
fetchHistoryData();
|
}
|
|
onMounted(() => {
|
fetchRealData();
|
pollTimer = window.setInterval(fetchRealData, POLL_INTERVAL);
|
});
|
|
onBeforeUnmount(() => {
|
if (pollTimer) {
|
window.clearInterval(pollTimer);
|
pollTimer = null;
|
}
|
});
|
</script>
|
|
<style scoped lang="scss">
|
.environment-monitoring-page {
|
min-height: 100%;
|
padding: 20px;
|
}
|
|
.chart-panel,
|
.table-panel {
|
padding: 20px;
|
border-radius: 16px;
|
background: #fff;
|
}
|
|
.table-panel {
|
margin-top: 20px;
|
}
|
|
.panel-title {
|
margin: 0 0 16px;
|
color: #1d344f;
|
font-size: 18px;
|
font-weight: 600;
|
}
|
|
.sensor-table,
|
.history-table {
|
display: flex;
|
flex-direction: column;
|
gap: 10px;
|
}
|
|
.sensor-table__head,
|
.sensor-table__row {
|
display: grid;
|
grid-template-columns: 1.2fr 1fr 0.9fr 0.9fr 1fr 0.9fr 0.9fr;
|
gap: 12px;
|
align-items: center;
|
}
|
|
.history-table__head,
|
.history-table__row {
|
display: grid;
|
grid-template-columns: 1.3fr 1fr 1fr 1fr 1fr;
|
gap: 12px;
|
align-items: center;
|
}
|
|
.sensor-table__head,
|
.history-table__head {
|
padding: 0 6px 10px;
|
color: #8393a8;
|
font-size: 13px;
|
}
|
|
.sensor-table__row,
|
.history-table__row {
|
padding: 14px 16px;
|
border-radius: 12px;
|
background: #f6f9fc;
|
color: #1d344f;
|
}
|
|
.sensor-table__action {
|
display: flex;
|
align-items: center;
|
}
|
|
.sensor-table__empty {
|
padding: 32px 0;
|
color: #8393a8;
|
text-align: center;
|
}
|
|
.history-toolbar {
|
display: flex;
|
justify-content: space-between;
|
gap: 16px;
|
align-items: center;
|
margin-bottom: 16px;
|
}
|
|
.history-toolbar__meta {
|
display: flex;
|
gap: 20px;
|
color: #1d344f;
|
flex-wrap: wrap;
|
}
|
|
.history-toolbar__filter {
|
display: flex;
|
gap: 12px;
|
align-items: center;
|
}
|
|
.history-table {
|
margin-top: 16px;
|
}
|
|
@media (max-width: 768px) {
|
.environment-monitoring-page {
|
padding: 12px;
|
}
|
|
.chart-panel,
|
.table-panel {
|
padding: 16px;
|
}
|
|
.sensor-table__head,
|
.sensor-table__row,
|
.history-table__head,
|
.history-table__row {
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
}
|
|
.history-toolbar {
|
display: block;
|
}
|
|
.history-toolbar__meta,
|
.history-toolbar__filter {
|
margin-bottom: 12px;
|
}
|
}
|
</style>
|