|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="app-container">
|
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
|
<el-form :inline="true" :model="queryForm" size="small">
|
|
|
|
|
|
<el-form-item label="时间范围">
|
|
|
|
|
|
<el-date-picker v-model="timeRange" type="datetimerange" value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
|
range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" style="width: 380px" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="采样间隔阈值">
|
|
|
|
|
|
<el-input-number v-model="queryForm.gapThresholdSeconds" :min="60" :max="86400" :step="60" style="width: 140px" />
|
|
|
|
|
|
<span class="ml-1 text-xs text-gray-400">秒</span>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="预期采样数">
|
|
|
|
|
|
<el-input-number v-model="queryForm.expectedSampleCount" :min="1" :max="100000" style="width: 140px" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item>
|
|
|
|
|
|
<el-button type="primary" icon="Search" @click="handleQuery">查询</el-button>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 入库延迟分布 + 数据完整率 -->
|
|
|
|
|
|
<el-row :gutter="16" class="mt-4">
|
|
|
|
|
|
<el-col :xs="24" :sm="12">
|
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
|
<template #header>
|
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
|
<span class="font-bold">入库延迟分布</span>
|
|
|
|
|
|
<HelpButton title="入库延迟分布" :content="QUALITY_HELP" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<div ref="delayDistChartRef" class="chart-container" />
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :xs="24" :sm="12">
|
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
|
<template #header>
|
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
|
<span class="font-bold">数据完整率</span>
|
|
|
|
|
|
<HelpButton title="数据完整率" :content="QUALITY_HELP" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<el-table :data="completenessList" stripe size="small" max-height="300">
|
|
|
|
|
|
<el-table-column prop="monitorName" label="测点" show-overflow-tooltip />
|
|
|
|
|
|
<el-table-column prop="actualCount" label="实际数" width="80" align="right" />
|
|
|
|
|
|
<el-table-column prop="completenessRate" label="完整率" width="120" align="right">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-progress :percentage="Math.round((row.completenessRate ?? 0) * 100)" :stroke-width="12"
|
|
|
|
|
|
:color="row.completenessRate >= 0.9 ? '#67c23a' : row.completenessRate >= 0.7 ? '#e6a23c' : '#f56c6c'" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 时间逆序可疑数据 -->
|
|
|
|
|
|
<el-card shadow="never" class="mt-4">
|
|
|
|
|
|
<template #header>
|
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
|
<span class="font-bold">时间逆序可疑数据(recodeTime < collectTime)</span>
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<el-tag type="danger" size="small">{{ timeReversalList.length }} 条</el-tag>
|
|
|
|
|
|
<HelpButton title="时间逆序可疑数据" :content="QUALITY_HELP" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<el-table :data="timeReversalList" stripe size="small" max-height="300">
|
|
|
|
|
|
<el-table-column prop="monitorId" label="测点ID" width="150" />
|
|
|
|
|
|
<el-table-column prop="monitorName" label="测点名称" width="180" />
|
|
|
|
|
|
<el-table-column prop="temperature" label="温度" width="80" align="right">
|
|
|
|
|
|
<template #default="{ row }">{{ row.temperature?.toFixed(2) }}℃</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="collectTime" label="采集时间" width="200" />
|
|
|
|
|
|
<el-table-column prop="recodeTime" label="入库时间" width="200" />
|
|
|
|
|
|
<el-table-column prop="delaySeconds" label="延迟(秒)" width="100" align="right">
|
|
|
|
|
|
<template #default="{ row }"><el-tag type="danger" size="small">{{ row.delaySeconds }}s</el-tag></template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 采样间隔异常 + 测点活跃度 -->
|
|
|
|
|
|
<el-row :gutter="16" class="mt-4">
|
|
|
|
|
|
<el-col :xs="24" :sm="12">
|
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
|
<template #header>
|
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
|
<span class="font-bold">采样间隔异常</span>
|
|
|
|
|
|
<HelpButton title="采样间隔异常" :content="QUALITY_HELP" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<el-table :data="samplingGapList" stripe size="small" max-height="300">
|
|
|
|
|
|
<el-table-column prop="monitorName" label="测点" show-overflow-tooltip />
|
|
|
|
|
|
<el-table-column prop="gapSeconds" label="间隔(秒)" width="100" align="right">
|
|
|
|
|
|
<template #default="{ row }"><el-tag type="warning" size="small">{{ row.gapSeconds }}s</el-tag></template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="prevTime" label="前次时间" width="160" />
|
|
|
|
|
|
<el-table-column prop="collectTime" label="本次时间" width="160" />
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :xs="24" :sm="12">
|
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
|
<template #header>
|
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
|
<span class="font-bold">测点活跃度</span>
|
|
|
|
|
|
<HelpButton title="测点活跃度" :content="QUALITY_HELP" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<el-table :data="activityList" stripe size="small" max-height="300">
|
|
|
|
|
|
<el-table-column prop="monitorName" label="测点" show-overflow-tooltip />
|
|
|
|
|
|
<el-table-column prop="actualCount" label="样本数" width="80" align="right" />
|
|
|
|
|
|
<el-table-column prop="firstTime" label="首次" width="160" />
|
|
|
|
|
|
<el-table-column prop="lastTime" label="末次" width="160" />
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { ref, reactive, onMounted, nextTick } from 'vue'
|
|
|
|
|
|
import * as echarts from 'echarts'
|
|
|
|
|
|
import { getDelayDistribution, getTimeReversal, getSamplingGapAnomalies, getCompletenessRate, getMonitorActivity } from '@/api/ems/report/tempBoard'
|
|
|
|
|
|
import type { TempBoardQuery, TempBoardQualityVO } from '@/api/ems/report/tempBoard'
|
|
|
|
|
|
import HelpButton from '../components/HelpButton.vue'
|
|
|
|
|
|
import { QUALITY_HELP } from '../components/helpContent'
|
|
|
|
|
|
import { useChartResize } from '../components/useChartResize'
|
|
|
|
|
|
|
|
|
|
|
|
defineOptions({ name: 'TempBoardQuality' })
|
|
|
|
|
|
|
|
|
|
|
|
const timeRange = ref<[string, string]>(['', ''])
|
|
|
|
|
|
const queryForm = reactive<TempBoardQuery>({ gapThresholdSeconds: 300, expectedSampleCount: 1440 })
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const delayDistChartRef = ref<HTMLElement>()
|
|
|
|
|
|
const timeReversalList = ref<TempBoardQualityVO[]>([])
|
|
|
|
|
|
const samplingGapList = ref<TempBoardQualityVO[]>([])
|
|
|
|
|
|
const completenessList = ref<TempBoardQualityVO[]>([])
|
|
|
|
|
|
const activityList = ref<TempBoardQualityVO[]>([])
|
|
|
|
|
|
|
|
|
|
|
|
const { getChart } = useChartResize(delayDistChartRef)
|
|
|
|
|
|
|
|
|
|
|
|
function initTimeRange() {
|
|
|
|
|
|
const end = new Date(); const start = new Date(end.getTime() - 24 * 3600 * 1000)
|
|
|
|
|
|
const p = (n: number) => n.toString().padStart(2, '0')
|
|
|
|
|
|
const fmt = (d: Date) => `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
|
|
|
|
|
|
timeRange.value = [fmt(start), fmt(end)]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleQuery() {
|
|
|
|
|
|
if (timeRange.value?.[0]) { queryForm.startTime = timeRange.value[0]; queryForm.endTime = timeRange.value[1] }
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const [ddRes, trRes, sgRes, crRes, actRes] = await Promise.all([
|
|
|
|
|
|
getDelayDistribution(queryForm), getTimeReversal(queryForm),
|
|
|
|
|
|
getSamplingGapAnomalies(queryForm), getCompletenessRate(queryForm), getMonitorActivity(queryForm)
|
|
|
|
|
|
])
|
|
|
|
|
|
timeReversalList.value = trRes.data; samplingGapList.value = sgRes.data
|
|
|
|
|
|
completenessList.value = crRes.data; activityList.value = actRes.data
|
|
|
|
|
|
await nextTick()
|
|
|
|
|
|
const chart = getChart(delayDistChartRef)
|
|
|
|
|
|
if (chart && ddRes.data.length) {
|
|
|
|
|
|
chart.setOption({
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
trigger: 'axis',
|
|
|
|
|
|
axisPointer: { type: 'shadow' },
|
|
|
|
|
|
formatter: (params: any) => `${params[0].name}<br/>样本数: <b>${params[0].value}</b>`
|
|
|
|
|
|
},
|
|
|
|
|
|
grid: { left: 60, right: 30, top: 30, bottom: 30 },
|
|
|
|
|
|
xAxis: { type: 'category', data: ddRes.data.map(d => d.delayBucket), axisLabel: { color: '#666' } },
|
|
|
|
|
|
yAxis: { type: 'value', name: '样本数', axisLabel: { color: '#666' }, splitLine: { lineStyle: { type: 'dashed' } } },
|
|
|
|
|
|
series: [{
|
|
|
|
|
|
type: 'bar',
|
|
|
|
|
|
data: ddRes.data.map(d => d.sampleCount),
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
|
|
|
{ offset: 0, color: '#e6a23c' },
|
|
|
|
|
|
{ offset: 1, color: '#f5deb3' }
|
|
|
|
|
|
]),
|
|
|
|
|
|
borderRadius: [4, 4, 0, 0]
|
|
|
|
|
|
},
|
|
|
|
|
|
label: { show: true, position: 'top', fontSize: 10, color: '#666' },
|
|
|
|
|
|
barMaxWidth: 30
|
|
|
|
|
|
}]
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally { loading.value = false }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => { initTimeRange(); handleQuery() })
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.card-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chart-container {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
min-height: 300px;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|