| | |
| | | <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.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 { getEnvironmentalRealData } from "@/api/inventoryManagement/environmentalMonitoring"; |
| | | 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" }, |
| | | { key: "humidity", label: "湿度", color: "#1ea7fd" }, |
| | | { key: "co2", label: "二氧化碳", color: "#12c48b" }, |
| | | { key: "light", label: "光照", color: "#8b5cf6" }, |
| | | { 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 = { |
| | |
| | | textStyle: { color: "#6c7c96" }, |
| | | }; |
| | | |
| | | const extractNumericValue = (rawValue) => { |
| | | 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; |
| | | }; |
| | | } |
| | | |
| | | const normalizeMetricObject = (source, index) => { |
| | | 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}`, |
| | |
| | | }; |
| | | |
| | | Object.entries(source || {}).forEach(([key, value]) => { |
| | | const rawText = String(value ?? ""); |
| | | |
| | | if (rawText.includes("℃")) { |
| | | 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); |
| | | 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(() => [ |
| | | { |
| | |
| | | latestDevices.value.map((item) => ({ |
| | | guid: item.guid, |
| | | name: item.name, |
| | | temperature: `${Number(item.temperature || 0).toFixed(2)}℃`, |
| | | humidity: `${Number(item.humidity || 0).toFixed(2)}%RH`, |
| | | co2: `${Number(item.co2 || 0).toFixed(2)}ppm`, |
| | | light: `${Number(item.light || 0).toFixed(2)}Lux`, |
| | | temperature: formatMetricValue(item.temperature, "℃"), |
| | | humidity: formatMetricValue(item.humidity, "%RH"), |
| | | co2: formatMetricValue(item.co2, "ppm"), |
| | | light: formatMetricValue(item.light, "Lux"), |
| | | })) |
| | | ); |
| | | |
| | | const fetchRealData = async () => { |
| | | 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 (error) { |
| | | } 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(); |
| | |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .sensor-table { |
| | | .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 1fr 1fr 1fr 1fr; |
| | | grid-template-columns: 1.2fr 1fr 0.9fr 0.9fr 1fr 0.9fr 0.9fr; |
| | | gap: 12px; |
| | | align-items: center; |
| | | } |
| | | |
| | | .sensor-table__head { |
| | | .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 { |
| | | .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) { |
| | |
| | | } |
| | | |
| | | .sensor-table__head, |
| | | .sensor-table__row { |
| | | .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> |