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