224 lines
6.1 KiB
Vue
224 lines
6.1 KiB
Vue
|
|
<template>
|
|||
|
|
<el-dialog
|
|||
|
|
v-model="historyDialogVisible"
|
|||
|
|
:title="dialogTitle"
|
|||
|
|
width="900px"
|
|||
|
|
destroy-on-close
|
|||
|
|
>
|
|||
|
|
<div class="history-toolbar">
|
|||
|
|
<el-form inline label-width="0">
|
|||
|
|
<el-form-item>
|
|||
|
|
<el-date-picker
|
|||
|
|
v-model="historyRange"
|
|||
|
|
type="datetimerange"
|
|||
|
|
range-separator="至"
|
|||
|
|
start-placeholder="开始时间"
|
|||
|
|
end-placeholder="结束时间"
|
|||
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|||
|
|
class="history-range-picker"
|
|||
|
|
@change="onHistorySearch"
|
|||
|
|
/>
|
|||
|
|
</el-form-item>
|
|||
|
|
<el-form-item>
|
|||
|
|
<el-button type="primary" @click="fetchHistoryData">查询</el-button>
|
|||
|
|
</el-form-item>
|
|||
|
|
</el-form>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="history-chart" v-loading="historyLoading">
|
|||
|
|
<template v-if="historyData.length">
|
|||
|
|
<Echart :key="echartKey" :options="historyChartOption" height="320px" />
|
|||
|
|
</template>
|
|||
|
|
<el-empty v-else description="暂无历史数据" />
|
|||
|
|
</div>
|
|||
|
|
</el-dialog>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import { computed, ref } from 'vue'
|
|||
|
|
import dayjs from 'dayjs'
|
|||
|
|
import { ElMessage } from 'element-plus'
|
|||
|
|
import type { EChartsOption } from 'echarts'
|
|||
|
|
|
|||
|
|
import { Echart } from '@/components/Echart'
|
|||
|
|
import { DeviceApi } from '@/api/iot/device/device/index'
|
|||
|
|
|
|||
|
|
interface LatestData {
|
|||
|
|
collectedAt?: number | string | null
|
|||
|
|
temperatureC?: number | string | null
|
|||
|
|
analog5?: number | string | null
|
|||
|
|
deviceType?: number | string | null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface ProductWithLatest {
|
|||
|
|
id: number
|
|||
|
|
name?: string
|
|||
|
|
latestData?: LatestData | null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const historyDialogVisible = ref(false)
|
|||
|
|
const selectedDevice = ref<ProductWithLatest | null>(null)
|
|||
|
|
const historyLoading = ref(false)
|
|||
|
|
const historyData = ref<any[]>([])
|
|||
|
|
const historyRange = ref<[string, string]>(['', ''])
|
|||
|
|
const echartKey = ref(0) // 强制 Echart 重新渲染,避免二次打开不显示
|
|||
|
|
|
|||
|
|
const dialogTitle = computed(() =>
|
|||
|
|
selectedDevice.value ? `${selectedDevice.value.name || '设备'}历史数据` : '设备历史数据'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const formatTimestamp = (value: number | string | null | undefined) => {
|
|||
|
|
if (value === null || value === undefined) return '-'
|
|||
|
|
const num = typeof value === 'number' ? value : Number(value)
|
|||
|
|
const ts = Number.isNaN(num) ? Date.parse(String(value)) : num < 1e12 ? num * 1000 : num
|
|||
|
|
if (!Number.isFinite(ts)) return '-'
|
|||
|
|
const date = dayjs(ts)
|
|||
|
|
return date.isValid() ? date.format('YYYY-MM-DD HH:mm:ss') : '-'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const toNumeric = (value: number | string | null | undefined) => {
|
|||
|
|
if (value === null || value === undefined || value === '') return null
|
|||
|
|
const num = typeof value === 'number' ? value : Number(value)
|
|||
|
|
return Number.isNaN(num) ? null : Number(num.toFixed(2))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const toTimestamp = (value: number | string | null | undefined) => {
|
|||
|
|
if (value === null || value === undefined || value === '') return null
|
|||
|
|
const num = typeof value === 'number' ? value : Number(value)
|
|||
|
|
const ts = Number.isNaN(num) ? Date.parse(String(value)) : num < 1e12 ? num * 1000 : num
|
|||
|
|
return Number.isFinite(ts) ? ts : null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 按时间升序展示,让最新数据在图表右侧
|
|||
|
|
const sortedHistoryList = computed(() => {
|
|||
|
|
const list = historyData.value || []
|
|||
|
|
return [...list].sort((a, b) => {
|
|||
|
|
const ta = toTimestamp(a?.collectedAt) || 0
|
|||
|
|
const tb = toTimestamp(b?.collectedAt) || 0
|
|||
|
|
return ta - tb
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const historyChartOption = computed<EChartsOption>(() => {
|
|||
|
|
const list = sortedHistoryList.value
|
|||
|
|
const times = list.map((item) => formatTimestamp(item.collectedAt))
|
|||
|
|
const temperatures = list.map((item) => toNumeric(item.temperatureC))
|
|||
|
|
const humidities = list.map((item) => toNumeric(item.analog5))
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
tooltip: {
|
|||
|
|
trigger: 'axis'
|
|||
|
|
},
|
|||
|
|
legend: {
|
|||
|
|
data: ['温度(℃)', '湿度(RH)']
|
|||
|
|
},
|
|||
|
|
grid: {
|
|||
|
|
left: 40,
|
|||
|
|
right: 40,
|
|||
|
|
top: 40,
|
|||
|
|
bottom: 40
|
|||
|
|
},
|
|||
|
|
xAxis: {
|
|||
|
|
type: 'category',
|
|||
|
|
boundaryGap: false,
|
|||
|
|
data: times
|
|||
|
|
},
|
|||
|
|
yAxis: [
|
|||
|
|
{
|
|||
|
|
type: 'value',
|
|||
|
|
name: '温度(℃)'
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
type: 'value',
|
|||
|
|
name: '湿度(RH)'
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
series: [
|
|||
|
|
{
|
|||
|
|
name: '温度(℃)',
|
|||
|
|
type: 'line',
|
|||
|
|
smooth: true,
|
|||
|
|
data: temperatures,
|
|||
|
|
showSymbol: false
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: '湿度(RH)',
|
|||
|
|
type: 'line',
|
|||
|
|
smooth: true,
|
|||
|
|
yAxisIndex: 1,
|
|||
|
|
data: humidities,
|
|||
|
|
showSymbol: false
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const resetHistoryRange = () => {
|
|||
|
|
const end = dayjs()
|
|||
|
|
const start = end.subtract(1, 'day')
|
|||
|
|
historyRange.value = [start.format('YYYY-MM-DD 00:00:00'), end.format('YYYY-MM-DD 23:59:59')]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const fetchHistoryData = async () => {
|
|||
|
|
if (!selectedDevice.value) return
|
|||
|
|
if (!historyRange.value?.[0] || !historyRange.value?.[1]) {
|
|||
|
|
ElMessage.warning('请选择时间范围')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
historyLoading.value = true
|
|||
|
|
try {
|
|||
|
|
const params = {
|
|||
|
|
deviceId: selectedDevice.value.id,
|
|||
|
|
startTime: historyRange.value[0],
|
|||
|
|
endTime: historyRange.value[1]
|
|||
|
|
}
|
|||
|
|
const res = await DeviceApi.getOeeBaseDataList(params)
|
|||
|
|
const data = (res as any)?.data ?? (res as any) ?? []
|
|||
|
|
historyData.value = Array.isArray(data) ? data : []
|
|||
|
|
echartKey.value += 1
|
|||
|
|
} catch (error) {
|
|||
|
|
historyData.value = []
|
|||
|
|
ElMessage.error('获取历史数据失败')
|
|||
|
|
} finally {
|
|||
|
|
historyLoading.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const onHistorySearch = () => {
|
|||
|
|
// 将结束时间调整为当天的 23:59:59(日期选择器默认是 00:00:00)
|
|||
|
|
if (historyRange.value?.[1]) {
|
|||
|
|
const end = dayjs(historyRange.value[1])
|
|||
|
|
// 如果结束时间是 00:00:00,说明是日期选择器默认值,需要调整为当天的 23:59:59
|
|||
|
|
if (end.format('HH:mm:ss') === '00:00:00') {
|
|||
|
|
historyRange.value[1] = end.format('YYYY-MM-DD') + ' 23:59:59'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
fetchHistoryData()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const openHistoryDialog = (device: ProductWithLatest) => {
|
|||
|
|
selectedDevice.value = device
|
|||
|
|
resetHistoryRange()
|
|||
|
|
historyDialogVisible.value = true
|
|||
|
|
fetchHistoryData()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
defineExpose({
|
|||
|
|
openHistoryDialog
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.history-toolbar {
|
|||
|
|
margin-bottom: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-range-picker {
|
|||
|
|
width: 360px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-chart {
|
|||
|
|
min-height: 320px;
|
|||
|
|
}
|
|||
|
|
</style>
|