feat(振动报表): 新增振动报表功能模块
新增振动报表功能模块,包含以下组件和页面: - 设备树组件(VibrationBoardDeviceTree.vue) - 筛选组件(VibrationBoardFilter.vue) - 共享工具函数(vibrationBoardShared.ts) - 查询状态管理(useVibrationBoardQueryState.ts) - 性能优化文档(performance-optimization.md) - 六个报表页面(overview, trend, comparison, quality, distribution, anomaly) - API接口文件(vibrationBoard.ts) 实现振动数据的多维度展示和分析功能,包括概览、趋势、对比、质量、分布和异常检测等报表页面main
parent
7156424030
commit
a6e72ddf82
@ -0,0 +1,131 @@
|
||||
import request from '@/utils/request';
|
||||
import type { EmsActionResponse, VibrationBoardQuery } from '../types';
|
||||
|
||||
export interface VibrationDistributionPageVO {
|
||||
metricField?: string;
|
||||
metricLabel?: string;
|
||||
unit?: string;
|
||||
intervalBuckets?: Array<{ label?: string; count?: number | string }>;
|
||||
histogramBuckets?: Array<{ startValue?: number | string; endValue?: number | string; count?: number | string }>;
|
||||
calendarHeatmap?: Array<{ statDate?: string; avgValue?: number | string }>;
|
||||
hourlyHeatmap?: Array<{ statDate?: string; statHour?: number | string; avgValue?: number | string }>;
|
||||
}
|
||||
|
||||
export interface VibrationOverviewPageVO {
|
||||
metricField?: string;
|
||||
metricLabel?: string;
|
||||
unit?: string;
|
||||
sampleCount?: number | string;
|
||||
deviceCount?: number | string;
|
||||
coverageRate?: number | string;
|
||||
metricCards?: Array<{ field?: string; label?: string; unit?: string; latest?: number | string; avg?: number | string; max?: number | string }>;
|
||||
gaugeItems?: Array<{ name?: string; value?: number | string; maxValue?: number | string; unit?: string }>;
|
||||
primaryMetricStats?: { latest?: number | string; min?: number | string; avg?: number | string; max?: number | string };
|
||||
deviceRanks?: Array<{
|
||||
monitorId?: string;
|
||||
monitorName?: string;
|
||||
avg?: number | string;
|
||||
latest?: number | string;
|
||||
max?: number | string;
|
||||
sampleCount?: number | string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface VibrationTrendPageVO {
|
||||
metricField?: string;
|
||||
metricLabel?: string;
|
||||
unit?: string;
|
||||
multiDevice?: boolean;
|
||||
series?: Array<{ name?: string; field?: string; unit?: string; points?: Array<{ time?: string; value?: number | string }> }>;
|
||||
hourlyItems?: Array<{ hour?: string; avgValue?: number | string }>;
|
||||
}
|
||||
|
||||
export interface VibrationComparisonPageVO {
|
||||
metricField?: string;
|
||||
metricLabel?: string;
|
||||
unit?: string;
|
||||
rankItems?: Array<{ monitorId?: string; monitorName?: string; avg?: number | string; latest?: number | string }>;
|
||||
scatterItems?: Array<{ monitorId?: string; monitorName?: string; avg?: number | string; max?: number | string; sampleCount?: number | string }>;
|
||||
}
|
||||
|
||||
export interface VibrationQualityPageVO {
|
||||
sampleCount?: number | string;
|
||||
deviceCount?: number | string;
|
||||
coverageRate?: number | string;
|
||||
metricQualityItems?: Array<{ field?: string; label?: string; unit?: string; validRate?: number | string; validCount?: number | string }>;
|
||||
}
|
||||
|
||||
export interface VibrationAnomalyPageVO {
|
||||
metricField?: string;
|
||||
metricLabel?: string;
|
||||
unit?: string;
|
||||
highThreshold?: number | string;
|
||||
warningThreshold?: number | string;
|
||||
rapidRiseThreshold?: number | string;
|
||||
stddevThreshold?: number | string;
|
||||
minContinuousSamples?: number | string;
|
||||
highEventCount?: number | string;
|
||||
continuousEventCount?: number | string;
|
||||
rapidRiseEventCount?: number | string;
|
||||
jitterEventCount?: number | string;
|
||||
highEvents?: Array<{ monitorId?: string; monitorName?: string; value?: number | string; recodeTime?: string }>;
|
||||
continuousEvents?: Array<{
|
||||
monitorId?: string;
|
||||
monitorName?: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
maxValue?: number | string;
|
||||
sampleCount?: number | string;
|
||||
}>;
|
||||
rapidRiseEvents?: Array<{ monitorId?: string; monitorName?: string; diff?: number | string; recodeTime?: string }>;
|
||||
jitterEvents?: Array<{ monitorId?: string; monitorName?: string; hourBucket?: string; stddev?: number | string; sampleCount?: number | string }>;
|
||||
}
|
||||
|
||||
export interface VibrationAdvancedPageVO {
|
||||
metricField?: string;
|
||||
metricLabel?: string;
|
||||
unit?: string;
|
||||
lowBandUpper?: number | string;
|
||||
focusBandUpper?: number | string;
|
||||
sankeyNodes?: Array<{ name?: string }>;
|
||||
sankeyLinks?: Array<{ source?: string; target?: string; value?: number | string }>;
|
||||
treemapItems?: Array<{ name?: string; value?: number | string; levelTag?: string }>;
|
||||
parallelAxes?: Array<{ dim?: number | string; name?: string; max?: number | string }>;
|
||||
parallelSeries?: Array<{ monitorId?: string; monitorName?: string; values?: Array<number | string> }>;
|
||||
}
|
||||
|
||||
function getPageData<T>(url: string, query?: VibrationBoardQuery): Promise<EmsActionResponse<T>> {
|
||||
return request({
|
||||
url,
|
||||
method: 'get',
|
||||
params: query
|
||||
});
|
||||
}
|
||||
|
||||
export function getVibrationOverviewData(query?: VibrationBoardQuery): Promise<EmsActionResponse<VibrationOverviewPageVO>> {
|
||||
return getPageData<VibrationOverviewPageVO>('/ems/report/vibrationBoard/overview', query);
|
||||
}
|
||||
|
||||
export function getVibrationTrendData(query?: VibrationBoardQuery): Promise<EmsActionResponse<VibrationTrendPageVO>> {
|
||||
return getPageData<VibrationTrendPageVO>('/ems/report/vibrationBoard/trend', query);
|
||||
}
|
||||
|
||||
export function getVibrationComparisonData(query?: VibrationBoardQuery): Promise<EmsActionResponse<VibrationComparisonPageVO>> {
|
||||
return getPageData<VibrationComparisonPageVO>('/ems/report/vibrationBoard/comparison', query);
|
||||
}
|
||||
|
||||
export function getVibrationQualityData(query?: VibrationBoardQuery): Promise<EmsActionResponse<VibrationQualityPageVO>> {
|
||||
return getPageData<VibrationQualityPageVO>('/ems/report/vibrationBoard/quality', query);
|
||||
}
|
||||
|
||||
export function getVibrationDistributionData(query?: VibrationBoardQuery): Promise<EmsActionResponse<VibrationDistributionPageVO>> {
|
||||
return getPageData<VibrationDistributionPageVO>('/ems/report/vibrationBoard/distribution', query);
|
||||
}
|
||||
|
||||
export function getVibrationAnomalyData(query?: VibrationBoardQuery): Promise<EmsActionResponse<VibrationAnomalyPageVO>> {
|
||||
return getPageData<VibrationAnomalyPageVO>('/ems/report/vibrationBoard/anomaly', query);
|
||||
}
|
||||
|
||||
export function getVibrationAdvancedData(query?: VibrationBoardQuery): Promise<EmsActionResponse<VibrationAdvancedPageVO>> {
|
||||
return getPageData<VibrationAdvancedPageVO>('/ems/report/vibrationBoard/advanced', query);
|
||||
}
|
||||
@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div v-loading="loading" class="app-container">
|
||||
<VibrationBoardFilter
|
||||
:selection-label="queryForm.selectionLabel"
|
||||
:time-range="daterangeRecordTime"
|
||||
:sampling-interval="queryForm.samplingInterval"
|
||||
:vibration-param="queryForm.vibrationParam"
|
||||
show-metric
|
||||
@update:time-range="setTimeRange"
|
||||
@update:sampling-interval="setSamplingInterval"
|
||||
@update:vibration-param="setVibrationParam"
|
||||
@query="handleQuery"
|
||||
/>
|
||||
|
||||
<el-row :gutter="16" class="mt-4">
|
||||
<el-col :span="6">
|
||||
<VibrationBoardDeviceTree :loading="treeLoading" :tree-data="monitorTreeOptions" :tree-props="treeProps" @node-click="handleTreeNodeClick" />
|
||||
</el-col>
|
||||
<el-col :span="18">
|
||||
<el-card>
|
||||
<template #header>振动流向图</template>
|
||||
<Chart ref="sankeyChart" class="chart-xl" />
|
||||
</el-card>
|
||||
<el-row :gutter="16" class="mt-4">
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>矩形树图</template>
|
||||
<Chart ref="treemapChart" class="chart-lg" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>平行坐标画像</template>
|
||||
<Chart ref="parallelChart" class="chart-lg" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Chart from '@/components/Charts/Chart.vue';
|
||||
import { getVibrationAdvancedData, type VibrationAdvancedPageVO } from '@/api/ems/report/vibrationBoard';
|
||||
import VibrationBoardFilter from '../components/VibrationBoardFilter.vue';
|
||||
import VibrationBoardDeviceTree from '../components/VibrationBoardDeviceTree.vue';
|
||||
import { useVibrationBoardQueryState } from '../components/useVibrationBoardQueryState';
|
||||
|
||||
defineOptions({ name: 'VibrationBoardAdvanced' });
|
||||
|
||||
const sankeyChart = ref<any>();
|
||||
const treemapChart = ref<any>();
|
||||
const parallelChart = ref<any>();
|
||||
|
||||
const {
|
||||
loading,
|
||||
treeLoading,
|
||||
monitorTreeOptions,
|
||||
treeProps,
|
||||
daterangeRecordTime,
|
||||
queryForm,
|
||||
loadTree,
|
||||
handleTreeNodeClick,
|
||||
setTimeRange,
|
||||
setSamplingInterval,
|
||||
setVibrationParam,
|
||||
buildQuery
|
||||
} = useVibrationBoardQueryState();
|
||||
|
||||
const advancedData = ref<VibrationAdvancedPageVO>({
|
||||
sankeyNodes: [],
|
||||
sankeyLinks: [],
|
||||
treemapItems: [],
|
||||
parallelAxes: [],
|
||||
parallelSeries: []
|
||||
});
|
||||
|
||||
const toNumber = (value: number | string | undefined) => Number(value ?? 0);
|
||||
|
||||
const renderCharts = () => {
|
||||
sankeyChart.value?.setData({
|
||||
tooltip: { trigger: 'item' },
|
||||
series: [
|
||||
{
|
||||
type: 'sankey',
|
||||
data: advancedData.value.sankeyNodes || [],
|
||||
links: (advancedData.value.sankeyLinks || []).map((item) => ({
|
||||
source: item.source,
|
||||
target: item.target,
|
||||
value: toNumber(item.value)
|
||||
})),
|
||||
emphasis: { focus: 'adjacency' },
|
||||
lineStyle: { color: 'gradient', curveness: 0.5 }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
treemapChart.value?.setData({
|
||||
tooltip: { trigger: 'item' },
|
||||
series: [
|
||||
{
|
||||
type: 'treemap',
|
||||
roam: false,
|
||||
data: (advancedData.value.treemapItems || []).map((item) => ({
|
||||
name: item.name,
|
||||
value: toNumber(item.value),
|
||||
itemStyle: {
|
||||
color: item.levelTag === '高位' ? '#f56c6c' : item.levelTag === '关注' ? '#e6a23c' : '#67c23a'
|
||||
}
|
||||
})),
|
||||
label: { show: true, formatter: '{b}\n{c}' }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
parallelChart.value?.setData({
|
||||
parallelAxis: (advancedData.value.parallelAxes || []).map((item) => ({
|
||||
dim: toNumber(item.dim),
|
||||
name: item.name,
|
||||
max: toNumber(item.max)
|
||||
})),
|
||||
parallel: { left: '10%', right: '10%', bottom: 40, top: 40 },
|
||||
series: [
|
||||
{
|
||||
type: 'parallel',
|
||||
lineStyle: { width: 1, opacity: 0.4 },
|
||||
data: (advancedData.value.parallelSeries || []).map((item) => item.values?.map((value) => toNumber(value)) || [])
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
const handleQuery = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await getVibrationAdvancedData(buildQuery());
|
||||
advancedData.value = data || {
|
||||
sankeyNodes: [],
|
||||
sankeyLinks: [],
|
||||
treemapItems: [],
|
||||
parallelAxes: [],
|
||||
parallelSeries: []
|
||||
};
|
||||
await nextTick();
|
||||
renderCharts();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadTree();
|
||||
await handleQuery();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-lg {
|
||||
height: 360px;
|
||||
}
|
||||
|
||||
.chart-xl {
|
||||
height: 420px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,203 @@
|
||||
<template>
|
||||
<div v-loading="loading" class="app-container">
|
||||
<VibrationBoardFilter
|
||||
:selection-label="queryForm.selectionLabel"
|
||||
:time-range="daterangeRecordTime"
|
||||
:sampling-interval="queryForm.samplingInterval"
|
||||
:vibration-param="queryForm.vibrationParam"
|
||||
show-metric
|
||||
@update:time-range="setTimeRange"
|
||||
@update:sampling-interval="setSamplingInterval"
|
||||
@update:vibration-param="setVibrationParam"
|
||||
@query="handleQuery"
|
||||
/>
|
||||
|
||||
<el-card shadow="never" class="mt-4">
|
||||
<el-form :inline="true" size="small" label-width="110px">
|
||||
<el-form-item label="高阈值">
|
||||
<el-input-number v-model="queryForm.highThreshold" :min="0" :precision="2" style="width: 140px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="预警阈值">
|
||||
<el-input-number v-model="queryForm.warningThreshold" :min="0" :precision="2" style="width: 140px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="连续样本数">
|
||||
<el-input-number v-model="queryForm.minContinuousSamples" :min="2" :max="100" style="width: 140px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="变化率阈值">
|
||||
<el-input-number v-model="queryForm.rapidRiseThreshold" :min="0.1" :precision="2" :step="0.1" style="width: 140px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="抖动阈值">
|
||||
<el-input-number v-model="queryForm.stddevThreshold" :min="0.1" :precision="2" :step="0.1" style="width: 140px" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-row :gutter="16" class="mt-4">
|
||||
<el-col :span="6"
|
||||
><el-card
|
||||
><div class="kpi-label">高风险事件</div>
|
||||
<div class="kpi-value danger">{{ anomalyData.highEventCount || 0 }}</div></el-card
|
||||
></el-col
|
||||
>
|
||||
<el-col :span="6"
|
||||
><el-card
|
||||
><div class="kpi-label">连续超标段</div>
|
||||
<div class="kpi-value warning">{{ anomalyData.continuousEventCount || 0 }}</div></el-card
|
||||
></el-col
|
||||
>
|
||||
<el-col :span="6"
|
||||
><el-card
|
||||
><div class="kpi-label">变化过快</div>
|
||||
<div class="kpi-value primary">{{ anomalyData.rapidRiseEventCount || 0 }}</div></el-card
|
||||
></el-col
|
||||
>
|
||||
<el-col :span="6"
|
||||
><el-card
|
||||
><div class="kpi-label">抖动异常</div>
|
||||
<div class="kpi-value purple">{{ anomalyData.jitterEventCount || 0 }}</div></el-card
|
||||
></el-col
|
||||
>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16" class="mt-4">
|
||||
<el-col :span="6">
|
||||
<VibrationBoardDeviceTree :loading="treeLoading" :tree-data="monitorTreeOptions" :tree-props="treeProps" @node-click="handleTreeNodeClick" />
|
||||
</el-col>
|
||||
<el-col :span="18">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>高风险事件明细</template>
|
||||
<el-table :data="anomalyData.highEvents || []" stripe size="small" max-height="320">
|
||||
<el-table-column prop="monitorName" label="设备" />
|
||||
<el-table-column prop="value" label="数值" width="120" />
|
||||
<el-table-column prop="recodeTime" label="时间" width="180" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>变化过快事件</template>
|
||||
<el-table :data="anomalyData.rapidRiseEvents || []" stripe size="small" max-height="320">
|
||||
<el-table-column prop="monitorName" label="设备" />
|
||||
<el-table-column prop="diff" label="变化量" width="120" />
|
||||
<el-table-column prop="recodeTime" label="时间" width="180" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card class="mt-4">
|
||||
<template #header>连续超标时段</template>
|
||||
<el-table :data="anomalyData.continuousEvents || []" stripe size="small" max-height="320">
|
||||
<el-table-column prop="monitorName" label="设备" />
|
||||
<el-table-column prop="startTime" label="开始时间" width="180" />
|
||||
<el-table-column prop="endTime" label="结束时间" width="180" />
|
||||
<el-table-column prop="maxValue" label="峰值" width="120" />
|
||||
<el-table-column prop="sampleCount" label="样本数" width="100" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt-4">
|
||||
<template #header>抖动异常</template>
|
||||
<el-table :data="anomalyData.jitterEvents || []" stripe size="small" max-height="320">
|
||||
<el-table-column prop="monitorName" label="设备" />
|
||||
<el-table-column prop="hourBucket" label="小时桶" width="180" />
|
||||
<el-table-column prop="stddev" label="标准差" width="120" />
|
||||
<el-table-column prop="sampleCount" label="样本数" width="100" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getVibrationAnomalyData, type VibrationAnomalyPageVO } from '@/api/ems/report/vibrationBoard';
|
||||
import VibrationBoardFilter from '../components/VibrationBoardFilter.vue';
|
||||
import VibrationBoardDeviceTree from '../components/VibrationBoardDeviceTree.vue';
|
||||
import { useVibrationBoardQueryState } from '../components/useVibrationBoardQueryState';
|
||||
|
||||
defineOptions({ name: 'VibrationBoardAnomaly' });
|
||||
|
||||
const {
|
||||
loading,
|
||||
treeLoading,
|
||||
monitorTreeOptions,
|
||||
treeProps,
|
||||
daterangeRecordTime,
|
||||
queryForm,
|
||||
loadTree,
|
||||
handleTreeNodeClick,
|
||||
setTimeRange,
|
||||
setSamplingInterval,
|
||||
setVibrationParam,
|
||||
buildQuery
|
||||
} = useVibrationBoardQueryState();
|
||||
|
||||
const anomalyData = ref<VibrationAnomalyPageVO>({
|
||||
highEvents: [],
|
||||
continuousEvents: [],
|
||||
rapidRiseEvents: [],
|
||||
jitterEvents: []
|
||||
});
|
||||
|
||||
const applyThresholdDefaults = (data?: VibrationAnomalyPageVO) => {
|
||||
if (!data) return;
|
||||
queryForm.value.highThreshold = Number(data.highThreshold ?? queryForm.value.highThreshold ?? 0);
|
||||
queryForm.value.warningThreshold = Number(data.warningThreshold ?? queryForm.value.warningThreshold ?? 0);
|
||||
queryForm.value.minContinuousSamples = Number(data.minContinuousSamples ?? queryForm.value.minContinuousSamples ?? 3);
|
||||
queryForm.value.rapidRiseThreshold = Number(data.rapidRiseThreshold ?? queryForm.value.rapidRiseThreshold ?? 0);
|
||||
queryForm.value.stddevThreshold = Number(data.stddevThreshold ?? queryForm.value.stddevThreshold ?? 0);
|
||||
};
|
||||
|
||||
const handleQuery = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await getVibrationAnomalyData(buildQuery());
|
||||
anomalyData.value = data || {
|
||||
highEvents: [],
|
||||
continuousEvents: [],
|
||||
rapidRiseEvents: [],
|
||||
jitterEvents: []
|
||||
};
|
||||
applyThresholdDefaults(data);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadTree();
|
||||
await handleQuery();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.kpi-label {
|
||||
color: #5b6573;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
margin-top: 12px;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.primary {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.purple {
|
||||
color: #9b59b6;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<div v-loading="loading" class="app-container">
|
||||
<VibrationBoardFilter
|
||||
:selection-label="queryForm.selectionLabel"
|
||||
:time-range="daterangeRecordTime"
|
||||
:sampling-interval="queryForm.samplingInterval"
|
||||
:vibration-param="queryForm.vibrationParam"
|
||||
show-metric
|
||||
@update:time-range="setTimeRange"
|
||||
@update:sampling-interval="setSamplingInterval"
|
||||
@update:vibration-param="setVibrationParam"
|
||||
@query="handleQuery"
|
||||
/>
|
||||
|
||||
<el-row :gutter="16" class="mt-4">
|
||||
<el-col :span="6">
|
||||
<VibrationBoardDeviceTree :loading="treeLoading" :tree-data="monitorTreeOptions" :tree-props="treeProps" @node-click="handleTreeNodeClick" />
|
||||
</el-col>
|
||||
<el-col :span="18">
|
||||
<el-alert type="info" :closable="false" show-icon title="对比分析页面建议选择设备组或全量振动设备,以获得更高价值的横向结果。" class="mb-4" />
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12"
|
||||
><el-card><template #header>设备排名对比图</template><Chart ref="rankChart" class="chart-lg" /></el-card
|
||||
></el-col>
|
||||
<el-col :span="12"
|
||||
><el-card><template #header>设备散点图</template><Chart ref="scatterChart" class="chart-lg" /></el-card
|
||||
></el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Chart from '@/components/Charts/Chart.vue';
|
||||
import { getVibrationComparisonData, type VibrationComparisonPageVO } from '@/api/ems/report/vibrationBoard';
|
||||
import VibrationBoardFilter from '../components/VibrationBoardFilter.vue';
|
||||
import VibrationBoardDeviceTree from '../components/VibrationBoardDeviceTree.vue';
|
||||
import { useVibrationBoardQueryState } from '../components/useVibrationBoardQueryState';
|
||||
import { vibrationMetricOptions } from '../components/vibrationBoardShared';
|
||||
|
||||
defineOptions({ name: 'VibrationBoardComparison' });
|
||||
|
||||
const rankChart = ref<any>();
|
||||
const scatterChart = ref<any>();
|
||||
const {
|
||||
loading,
|
||||
treeLoading,
|
||||
monitorTreeOptions,
|
||||
treeProps,
|
||||
daterangeRecordTime,
|
||||
queryForm,
|
||||
loadTree,
|
||||
handleTreeNodeClick,
|
||||
setTimeRange,
|
||||
setSamplingInterval,
|
||||
setVibrationParam,
|
||||
buildQuery
|
||||
} = useVibrationBoardQueryState();
|
||||
const comparisonData = ref<VibrationComparisonPageVO>({ rankItems: [], scatterItems: [] });
|
||||
const metricColorMap = Object.fromEntries(vibrationMetricOptions.map((item) => [item.field, item.color]));
|
||||
const toNumber = (value: number | string | undefined) => Number(value ?? 0);
|
||||
const formatValue = (value: number | string | undefined) => (value === null || value === undefined || value === '' ? '--' : Number(value).toFixed(2));
|
||||
|
||||
const renderCharts = () => {
|
||||
rankChart.value?.setData({
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { top: 8, data: ['均值', '最新值'] },
|
||||
xAxis: { type: 'value' },
|
||||
yAxis: { type: 'category', data: (comparisonData.value.rankItems || []).map((item) => item.monitorName) },
|
||||
series: [
|
||||
{
|
||||
name: '均值',
|
||||
type: 'bar',
|
||||
data: (comparisonData.value.rankItems || []).map((item) => toNumber(item.avg)),
|
||||
itemStyle: { color: metricColorMap[comparisonData.value.metricField || queryForm.value.vibrationParam] || '#5b8ff9' }
|
||||
},
|
||||
{
|
||||
name: '最新值',
|
||||
type: 'bar',
|
||||
data: (comparisonData.value.rankItems || []).map((item) => toNumber(item.latest)),
|
||||
itemStyle: { color: '#36cfc9' }
|
||||
}
|
||||
]
|
||||
});
|
||||
scatterChart.value?.setData({
|
||||
tooltip: {
|
||||
formatter: (params: any) =>
|
||||
[
|
||||
`设备: ${params.value[3]}`,
|
||||
`均值: ${formatValue(params.value[0])}${comparisonData.value.unit || ''}`,
|
||||
`峰值: ${formatValue(params.value[1])}${comparisonData.value.unit || ''}`,
|
||||
`采样量: ${params.value[2]}`
|
||||
].join('<br/>')
|
||||
},
|
||||
xAxis: { type: 'value', name: `均值(${comparisonData.value.unit || ''})` },
|
||||
yAxis: { type: 'value', name: `峰值(${comparisonData.value.unit || ''})` },
|
||||
series: [
|
||||
{
|
||||
type: 'scatter',
|
||||
symbolSize: (value: any[]) => Math.max(12, Math.min(30, value[2] * 2)),
|
||||
itemStyle: { color: metricColorMap[comparisonData.value.metricField || queryForm.value.vibrationParam] || '#5b8ff9', opacity: 0.8 },
|
||||
label: { show: true, formatter: (params: any) => params.value[3], position: 'top', fontSize: 11 },
|
||||
data: (comparisonData.value.scatterItems || []).map((item) => [
|
||||
toNumber(item.avg),
|
||||
toNumber(item.max),
|
||||
toNumber(item.sampleCount),
|
||||
item.monitorName
|
||||
])
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
const handleQuery = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await getVibrationComparisonData(buildQuery());
|
||||
comparisonData.value = data || { rankItems: [], scatterItems: [] };
|
||||
await nextTick();
|
||||
renderCharts();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadTree();
|
||||
await handleQuery();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-lg {
|
||||
height: 420px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<el-card shadow="never" v-loading="loading">
|
||||
<div class="panel-title">振动设备树</div>
|
||||
<el-tree
|
||||
:data="treeData"
|
||||
:props="treeProps"
|
||||
default-expand-all
|
||||
highlight-current
|
||||
:expand-on-click-node="false"
|
||||
@node-click="emit('nodeClick', $event)"
|
||||
/>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
loading: boolean;
|
||||
treeData: any[];
|
||||
treeProps: { label: string; children: string };
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
nodeClick: [any];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.panel-title {
|
||||
margin-bottom: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<el-form :inline="true" :model="queryForm" size="small" label-width="86px">
|
||||
<el-form-item label="范围名称">
|
||||
<el-input :model-value="selectionLabel" disabled style="width: 220px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="记录时间">
|
||||
<el-date-picker
|
||||
v-model="localTimeRange"
|
||||
type="datetimerange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
range-separator="-"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
style="width: 340px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="采样间隔">
|
||||
<el-input-number v-model="localSamplingInterval" :min="1" :max="1440" controls-position="right" style="width: 122px" />
|
||||
<span class="form-suffix">分钟/点</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="showMetric" label="主看参数">
|
||||
<el-select v-model="localVibrationParam" style="width: 160px" @change="$emit('query')">
|
||||
<el-option v-for="item in vibrationMetricOptions" :key="item.field" :label="item.label" :value="item.field" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="$emit('query')">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { vibrationMetricOptions } from './vibrationBoardShared';
|
||||
|
||||
const props = defineProps<{
|
||||
selectionLabel: string;
|
||||
timeRange: string[];
|
||||
samplingInterval: number;
|
||||
vibrationParam?: string;
|
||||
showMetric?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:timeRange': [string[]];
|
||||
'update:samplingInterval': [number];
|
||||
'update:vibrationParam': [string];
|
||||
query: [];
|
||||
}>();
|
||||
|
||||
const queryForm = computed(() => props);
|
||||
const localTimeRange = computed({
|
||||
get: () => props.timeRange,
|
||||
set: (value) => emit('update:timeRange', value)
|
||||
});
|
||||
const localSamplingInterval = computed({
|
||||
get: () => props.samplingInterval,
|
||||
set: (value) => emit('update:samplingInterval', value)
|
||||
});
|
||||
const localVibrationParam = computed({
|
||||
get: () => props.vibrationParam || 'vibrationSpeed',
|
||||
set: (value) => emit('update:vibrationParam', value)
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-suffix {
|
||||
margin-left: 6px;
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,131 @@
|
||||
import { getMonitorInfoTree } from '@/api/ems/base/baseMonitorInfo';
|
||||
import type { VibrationBoardQuery } from '@/api/ems/types';
|
||||
import { createDefaultTimeRange } from './vibrationBoardShared';
|
||||
|
||||
export function useVibrationBoardQueryState(defaultMetric = 'vibrationSpeed') {
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
treeLoading: false,
|
||||
monitorTreeOptions: [] as any[],
|
||||
treeProps: { label: 'label', children: 'children' },
|
||||
deviceDisplayMap: {} as Record<string, string>,
|
||||
daterangeRecordTime: createDefaultTimeRange() as string[],
|
||||
queryForm: {
|
||||
compareScope: 'all',
|
||||
monitorId: '',
|
||||
monitorIds: [] as string[],
|
||||
selectionLabel: '全部振动设备',
|
||||
samplingInterval: 5,
|
||||
vibrationParam: defaultMetric,
|
||||
highThreshold: undefined as number | undefined,
|
||||
warningThreshold: undefined as number | undefined,
|
||||
minContinuousSamples: undefined as number | undefined,
|
||||
rapidRiseThreshold: undefined as number | undefined,
|
||||
stddevThreshold: undefined as number | undefined
|
||||
}
|
||||
});
|
||||
|
||||
const { loading, treeLoading, monitorTreeOptions, treeProps, deviceDisplayMap, daterangeRecordTime, queryForm } = toRefs(state);
|
||||
const deviceCount = computed(() => queryForm.value.monitorIds?.length || (queryForm.value.monitorId ? 1 : 0));
|
||||
const hasMultiDevice = computed(() => deviceCount.value > 1 || ['group', 'all'].includes(queryForm.value.compareScope));
|
||||
|
||||
const buildDeviceDisplayMap = (nodes: any[]): Record<string, string> =>
|
||||
(nodes || []).reduce(
|
||||
(acc, item) => {
|
||||
if (item.code && !item.children?.length) acc[item.code] = item.label;
|
||||
if (item.children?.length) Object.assign(acc, buildDeviceDisplayMap(item.children));
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
|
||||
const getLeafNodes = (nodes: any[]): any[] =>
|
||||
(nodes || []).flatMap((item) => (item.children?.length ? getLeafNodes(item.children) : item.code ? [item] : []));
|
||||
|
||||
const buildSelection = (nodes: any[], selectionLabel: string, compareScope?: string) => {
|
||||
const monitorIds = nodes.map((item) => item.code);
|
||||
return {
|
||||
compareScope: compareScope || (monitorIds.length > 1 ? 'group' : 'single'),
|
||||
monitorId: monitorIds.length === 1 ? monitorIds[0] : '',
|
||||
monitorIds,
|
||||
selectionLabel
|
||||
};
|
||||
};
|
||||
|
||||
const applySelection = (selection: any) => {
|
||||
Object.assign(queryForm.value, selection);
|
||||
};
|
||||
|
||||
const setTimeRange = (value: string[]) => {
|
||||
daterangeRecordTime.value = value;
|
||||
};
|
||||
|
||||
const setSamplingInterval = (value: number) => {
|
||||
queryForm.value.samplingInterval = value;
|
||||
};
|
||||
|
||||
const setVibrationParam = (value: string) => {
|
||||
queryForm.value.vibrationParam = value;
|
||||
};
|
||||
|
||||
const loadTree = async () => {
|
||||
treeLoading.value = true;
|
||||
try {
|
||||
const response = await getMonitorInfoTree({ monitorType: 10 });
|
||||
monitorTreeOptions.value = response.data || [];
|
||||
deviceDisplayMap.value = buildDeviceDisplayMap(monitorTreeOptions.value);
|
||||
if (!queryForm.value.monitorIds.length && !queryForm.value.monitorId) {
|
||||
applySelection(buildSelection(getLeafNodes(monitorTreeOptions.value), '全部振动设备', 'all'));
|
||||
}
|
||||
} finally {
|
||||
treeLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleTreeNodeClick = (data: any) => {
|
||||
if (!data) return;
|
||||
if (data.children?.length) {
|
||||
applySelection(buildSelection(getLeafNodes(data.children), `${data.label}设备组`, 'group'));
|
||||
return;
|
||||
}
|
||||
applySelection(buildSelection([data], data.label, 'single'));
|
||||
};
|
||||
|
||||
const buildQuery = (): VibrationBoardQuery => ({
|
||||
samplingInterval: queryForm.value.samplingInterval,
|
||||
vibrationParam: queryForm.value.vibrationParam,
|
||||
beginRecordTime: daterangeRecordTime.value[0],
|
||||
endRecordTime: daterangeRecordTime.value[1],
|
||||
highThreshold: queryForm.value.highThreshold,
|
||||
warningThreshold: queryForm.value.warningThreshold,
|
||||
minContinuousSamples: queryForm.value.minContinuousSamples,
|
||||
rapidRiseThreshold: queryForm.value.rapidRiseThreshold,
|
||||
stddevThreshold: queryForm.value.stddevThreshold,
|
||||
monitorId: queryForm.value.monitorIds?.length > 1 ? undefined : queryForm.value.monitorIds?.[0] || queryForm.value.monitorId,
|
||||
monitorIds: queryForm.value.monitorIds?.length > 1 ? queryForm.value.monitorIds : undefined,
|
||||
params: {
|
||||
beginRecordTime: daterangeRecordTime.value[0],
|
||||
endRecordTime: daterangeRecordTime.value[1]
|
||||
}
|
||||
});
|
||||
|
||||
const getMonitorDisplayName = (monitorId?: string) => (monitorId ? deviceDisplayMap.value[monitorId] || monitorId : '--');
|
||||
|
||||
return {
|
||||
loading,
|
||||
treeLoading,
|
||||
monitorTreeOptions,
|
||||
treeProps,
|
||||
daterangeRecordTime,
|
||||
queryForm,
|
||||
deviceCount,
|
||||
hasMultiDevice,
|
||||
loadTree,
|
||||
handleTreeNodeClick,
|
||||
setTimeRange,
|
||||
setSamplingInterval,
|
||||
setVibrationParam,
|
||||
buildQuery,
|
||||
getMonitorDisplayName
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
export const vibrationMetricOptions = [
|
||||
{ field: 'vibrationSpeed', label: '振动速度', unit: 'mm/s', color: '#f59e0b' },
|
||||
{ field: 'vibrationDisplacement', label: '振动位移', unit: 'um', color: '#5b8ff9' },
|
||||
{ field: 'vibrationAcceleration', label: '振动加速度', unit: 'g', color: '#36cfc9' },
|
||||
{ field: 'vibrationTemp', label: '振动温度', unit: '℃', color: '#f56c6c' }
|
||||
] as const;
|
||||
|
||||
export const formatDateTime = (date: Date) => {
|
||||
const p = (n: number) => n.toString().padStart(2, '0');
|
||||
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(date.getMinutes())}:${p(date.getSeconds())}`;
|
||||
};
|
||||
|
||||
export const createDefaultTimeRange = () => {
|
||||
const end = new Date();
|
||||
const start = new Date(end.getTime() - 24 * 3600 * 1000);
|
||||
return [formatDateTime(start), formatDateTime(end)];
|
||||
};
|
||||
@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<div v-loading="loading" class="app-container">
|
||||
<VibrationBoardFilter
|
||||
:selection-label="queryForm.selectionLabel"
|
||||
:time-range="daterangeRecordTime"
|
||||
:sampling-interval="queryForm.samplingInterval"
|
||||
:vibration-param="queryForm.vibrationParam"
|
||||
show-metric
|
||||
@update:time-range="setTimeRange"
|
||||
@update:sampling-interval="setSamplingInterval"
|
||||
@update:vibration-param="setVibrationParam"
|
||||
@query="handleQuery"
|
||||
/>
|
||||
|
||||
<el-row :gutter="16" class="mt-4">
|
||||
<el-col :span="6">
|
||||
<VibrationBoardDeviceTree :loading="treeLoading" :tree-data="monitorTreeOptions" :tree-props="treeProps" @node-click="handleTreeNodeClick" />
|
||||
</el-col>
|
||||
<el-col :span="18">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>区间分布</template>
|
||||
<Chart ref="intervalChart" class="chart-lg" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>直方图</template>
|
||||
<Chart ref="histogramChart" class="chart-lg" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-card class="mt-4">
|
||||
<template #header>日历热力图</template>
|
||||
<Chart ref="calendarChart" class="chart-lg" />
|
||||
</el-card>
|
||||
<el-card class="mt-4">
|
||||
<template #header>小时热力图</template>
|
||||
<Chart ref="hourlyChart" class="chart-xl" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Chart from '@/components/Charts/Chart.vue';
|
||||
import { getVibrationDistributionData, type VibrationDistributionPageVO } from '@/api/ems/report/vibrationBoard';
|
||||
import VibrationBoardFilter from '../components/VibrationBoardFilter.vue';
|
||||
import VibrationBoardDeviceTree from '../components/VibrationBoardDeviceTree.vue';
|
||||
import { useVibrationBoardQueryState } from '../components/useVibrationBoardQueryState';
|
||||
|
||||
defineOptions({ name: 'VibrationBoardDistribution' });
|
||||
|
||||
const intervalChart = ref<any>();
|
||||
const histogramChart = ref<any>();
|
||||
const calendarChart = ref<any>();
|
||||
const hourlyChart = ref<any>();
|
||||
|
||||
const {
|
||||
loading,
|
||||
treeLoading,
|
||||
monitorTreeOptions,
|
||||
treeProps,
|
||||
daterangeRecordTime,
|
||||
queryForm,
|
||||
loadTree,
|
||||
handleTreeNodeClick,
|
||||
setTimeRange,
|
||||
setSamplingInterval,
|
||||
setVibrationParam,
|
||||
buildQuery
|
||||
} = useVibrationBoardQueryState();
|
||||
|
||||
const distributionData = ref<VibrationDistributionPageVO>({
|
||||
intervalBuckets: [],
|
||||
histogramBuckets: [],
|
||||
calendarHeatmap: [],
|
||||
hourlyHeatmap: []
|
||||
});
|
||||
|
||||
const toNumber = (value: number | string | undefined) => Number(value ?? 0);
|
||||
|
||||
const renderCharts = () => {
|
||||
const intervalData = distributionData.value.intervalBuckets || [];
|
||||
const histogramData = distributionData.value.histogramBuckets || [];
|
||||
const calendarData = (distributionData.value.calendarHeatmap || []).map((item) => [item.statDate, toNumber(item.avgValue)]);
|
||||
const hourlyData = (distributionData.value.hourlyHeatmap || []).map((item) => [toNumber(item.statHour), item.statDate, toNumber(item.avgValue)]);
|
||||
const days = [...new Set(hourlyData.map((item) => item[1] as string))].sort();
|
||||
if (!intervalData.length && !histogramData.length && !calendarData.length && !hourlyData.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
intervalChart.value?.setData({
|
||||
tooltip: { trigger: 'item' },
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
data: intervalData.map((item) => ({ name: item.label, value: toNumber(item.count) })),
|
||||
label: { formatter: '{b}\n{d}%' }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
histogramChart.value?.setData({
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: { type: 'category', data: histogramData.map((item) => `${toNumber(item.startValue).toFixed(1)}~${toNumber(item.endValue).toFixed(1)}`) },
|
||||
yAxis: { type: 'value' },
|
||||
series: [{ type: 'bar', data: histogramData.map((item) => toNumber(item.count)), itemStyle: { color: '#5b8ff9' } }]
|
||||
});
|
||||
|
||||
calendarChart.value?.setData({
|
||||
tooltip: { trigger: 'item' },
|
||||
visualMap: {
|
||||
min: calendarData.length ? Math.min(...calendarData.map((item) => item[1] as number)) : 0,
|
||||
max: calendarData.length ? Math.max(...calendarData.map((item) => item[1] as number)) : 0,
|
||||
calculable: true,
|
||||
orient: 'horizontal',
|
||||
left: 'center',
|
||||
bottom: 0
|
||||
},
|
||||
calendar: { range: calendarData.length ? [calendarData[0][0], calendarData[calendarData.length - 1][0]] : [] },
|
||||
series: [{ type: 'heatmap', coordinateSystem: 'calendar', data: calendarData }]
|
||||
});
|
||||
|
||||
hourlyChart.value?.setData({
|
||||
tooltip: { trigger: 'item' },
|
||||
xAxis: { type: 'category', data: Array.from({ length: 24 }, (_, index) => `${index.toString().padStart(2, '0')}:00`) },
|
||||
yAxis: { type: 'category', data: days },
|
||||
visualMap: {
|
||||
min: hourlyData.length ? Math.min(...hourlyData.map((item) => item[2] as number)) : 0,
|
||||
max: hourlyData.length ? Math.max(...hourlyData.map((item) => item[2] as number)) : 0,
|
||||
calculable: true,
|
||||
orient: 'horizontal',
|
||||
left: 'center',
|
||||
bottom: 0
|
||||
},
|
||||
series: [{ type: 'heatmap', data: hourlyData }]
|
||||
});
|
||||
};
|
||||
|
||||
const handleQuery = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await getVibrationDistributionData(buildQuery());
|
||||
distributionData.value = data || {
|
||||
intervalBuckets: [],
|
||||
histogramBuckets: [],
|
||||
calendarHeatmap: [],
|
||||
hourlyHeatmap: []
|
||||
};
|
||||
await nextTick();
|
||||
renderCharts();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadTree();
|
||||
await handleQuery();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-lg {
|
||||
height: 360px;
|
||||
}
|
||||
|
||||
.chart-xl {
|
||||
height: 420px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<div v-loading="loading" class="app-container">
|
||||
<VibrationBoardFilter
|
||||
:selection-label="queryForm.selectionLabel"
|
||||
:time-range="daterangeRecordTime"
|
||||
:sampling-interval="queryForm.samplingInterval"
|
||||
:vibration-param="queryForm.vibrationParam"
|
||||
show-metric
|
||||
@update:time-range="setTimeRange"
|
||||
@update:sampling-interval="setSamplingInterval"
|
||||
@update:vibration-param="setVibrationParam"
|
||||
@query="handleQuery"
|
||||
/>
|
||||
|
||||
<el-row :gutter="16" class="mt-4">
|
||||
<el-col :span="6"
|
||||
><el-card
|
||||
><div class="kpi-label">采样记录数</div>
|
||||
<div class="kpi-value">{{ toNumber(overviewData.sampleCount) }}</div></el-card
|
||||
></el-col
|
||||
>
|
||||
<el-col :span="6"
|
||||
><el-card
|
||||
><div class="kpi-label">设备数量</div>
|
||||
<div class="kpi-value">{{ toNumber(overviewData.deviceCount) }}</div></el-card
|
||||
></el-col
|
||||
>
|
||||
<el-col :span="6"
|
||||
><el-card
|
||||
><div class="kpi-label">主看指标</div>
|
||||
<div class="kpi-value">{{ overviewData.metricLabel || '--' }}</div></el-card
|
||||
></el-col
|
||||
>
|
||||
<el-col :span="6"
|
||||
><el-card
|
||||
><div class="kpi-label">覆盖率</div>
|
||||
<div class="kpi-value">{{ formatPercent(toNumber(overviewData.coverageRate)) }}</div></el-card
|
||||
></el-col
|
||||
>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16" class="mt-4">
|
||||
<el-col :span="6" v-for="item in overviewData.metricCards || []" :key="item.field">
|
||||
<el-card class="metric-card" :style="{ borderTopColor: metricColorMap[item.field || ''] || '#5b8ff9' }">
|
||||
<div class="kpi-label">{{ item.label }}</div>
|
||||
<div class="kpi-value">
|
||||
{{ formatValue(item.latest) }}<span class="unit">{{ item.unit }}</span>
|
||||
</div>
|
||||
<div class="metric-foot">均值 {{ formatValue(item.avg) }}{{ item.unit }}</div>
|
||||
<div class="metric-foot">峰值 {{ formatValue(item.max) }}{{ item.unit }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16" class="mt-4">
|
||||
<el-col :span="6">
|
||||
<VibrationBoardDeviceTree :loading="treeLoading" :tree-data="monitorTreeOptions" :tree-props="treeProps" @node-click="handleTreeNodeClick" />
|
||||
</el-col>
|
||||
<el-col :span="18">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12"
|
||||
><el-card><template #header>仪表盘总览</template><Chart ref="gaugeChart" class="chart-lg" /></el-card
|
||||
></el-col>
|
||||
<el-col :span="12"
|
||||
><el-card
|
||||
><template #header>{{ hasMultiDevice ? '设备排名对比图' : '主看指标分布图' }}</template
|
||||
><Chart ref="rankChart" class="chart-lg" /></el-card
|
||||
></el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Chart from '@/components/Charts/Chart.vue';
|
||||
import { getVibrationOverviewData, type VibrationOverviewPageVO } from '@/api/ems/report/vibrationBoard';
|
||||
import VibrationBoardFilter from '../components/VibrationBoardFilter.vue';
|
||||
import VibrationBoardDeviceTree from '../components/VibrationBoardDeviceTree.vue';
|
||||
import { useVibrationBoardQueryState } from '../components/useVibrationBoardQueryState';
|
||||
import { vibrationMetricOptions } from '../components/vibrationBoardShared';
|
||||
|
||||
defineOptions({ name: 'VibrationBoardOverview' });
|
||||
|
||||
const gaugeChart = ref<any>();
|
||||
const rankChart = ref<any>();
|
||||
const {
|
||||
loading,
|
||||
treeLoading,
|
||||
monitorTreeOptions,
|
||||
treeProps,
|
||||
daterangeRecordTime,
|
||||
queryForm,
|
||||
deviceCount,
|
||||
hasMultiDevice,
|
||||
loadTree,
|
||||
handleTreeNodeClick,
|
||||
setTimeRange,
|
||||
setSamplingInterval,
|
||||
setVibrationParam,
|
||||
buildQuery
|
||||
} = useVibrationBoardQueryState();
|
||||
const overviewData = ref<VibrationOverviewPageVO>({ metricCards: [], gaugeItems: [], deviceRanks: [] });
|
||||
const metricColorMap = Object.fromEntries(vibrationMetricOptions.map((item) => [item.field, item.color]));
|
||||
|
||||
const toNumber = (value: number | string | undefined) => Number(value ?? 0);
|
||||
const formatValue = (value: number | string | undefined) => (value === null || value === undefined || value === '' ? '--' : Number(value).toFixed(2));
|
||||
const formatPercent = (value: number) => `${(Number(value || 0) * 100).toFixed(1)}%`;
|
||||
|
||||
const renderCharts = () => {
|
||||
const gaugeItems = overviewData.value.gaugeItems || [];
|
||||
const deviceRanks = overviewData.value.deviceRanks || [];
|
||||
if (gaugeItems.length) {
|
||||
gaugeChart.value?.setData({
|
||||
series: gaugeItems.map((item, index) => ({
|
||||
type: 'gauge',
|
||||
min: 0,
|
||||
max: toNumber(item.maxValue),
|
||||
radius: '78%',
|
||||
center: [
|
||||
['18%', '55%'],
|
||||
['50%', '55%'],
|
||||
['82%', '55%']
|
||||
][index] || ['50%', '55%'],
|
||||
progress: { show: true, width: 12, itemStyle: { color: ['#5b8ff9', '#36cfc9', '#f59e0b'][index] || '#5b8ff9' } },
|
||||
axisLine: { lineStyle: { width: 12 } },
|
||||
pointer: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { length: 10 },
|
||||
axisLabel: { distance: 16, fontSize: 10 },
|
||||
title: { offsetCenter: [0, '82%'], fontSize: 12 },
|
||||
detail: { offsetCenter: [0, '22%'], fontSize: 18, formatter: `${formatValue(item.value)} ${item.unit || ''}` },
|
||||
data: [{ value: toNumber(item.value), name: item.name }]
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
if (hasMultiDevice.value) {
|
||||
rankChart.value?.setData({
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: { type: 'value' },
|
||||
yAxis: { type: 'category', data: deviceRanks.map((item) => item.monitorName) },
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
data: deviceRanks.map((item) => toNumber(item.avg)),
|
||||
itemStyle: { color: metricColorMap[queryForm.value.vibrationParam] || '#5b8ff9' }
|
||||
}
|
||||
]
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = overviewData.value.primaryMetricStats;
|
||||
if (!stats) return;
|
||||
rankChart.value?.setData({
|
||||
xAxis: { type: 'category', data: ['最小值', '均值', '最大值'] },
|
||||
yAxis: { type: 'value', name: overviewData.value.unit || '' },
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
data: [toNumber(stats.min), toNumber(stats.avg), toNumber(stats.max)],
|
||||
itemStyle: { color: metricColorMap[queryForm.value.vibrationParam] || '#5b8ff9' }
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
const handleQuery = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await getVibrationOverviewData(buildQuery());
|
||||
overviewData.value = data || { metricCards: [], gaugeItems: [], deviceRanks: [] };
|
||||
await nextTick();
|
||||
renderCharts();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadTree();
|
||||
await handleQuery();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.kpi-label {
|
||||
color: #5b6573;
|
||||
font-size: 13px;
|
||||
}
|
||||
.kpi-value {
|
||||
margin-top: 12px;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #1f2329;
|
||||
}
|
||||
.metric-card {
|
||||
border-top: 4px solid #5b8ff9;
|
||||
}
|
||||
.metric-foot {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
}
|
||||
.unit {
|
||||
margin-left: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.chart-lg {
|
||||
height: 420px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div v-loading="loading" class="app-container">
|
||||
<VibrationBoardFilter
|
||||
:selection-label="queryForm.selectionLabel"
|
||||
:time-range="daterangeRecordTime"
|
||||
:sampling-interval="queryForm.samplingInterval"
|
||||
:vibration-param="queryForm.vibrationParam"
|
||||
@update:time-range="setTimeRange"
|
||||
@update:sampling-interval="setSamplingInterval"
|
||||
@update:vibration-param="setVibrationParam"
|
||||
@query="handleQuery"
|
||||
/>
|
||||
|
||||
<el-row :gutter="16" class="mt-4">
|
||||
<el-col :span="6">
|
||||
<VibrationBoardDeviceTree :loading="treeLoading" :tree-data="monitorTreeOptions" :tree-props="treeProps" @node-click="handleTreeNodeClick" />
|
||||
</el-col>
|
||||
<el-col :span="18">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8"
|
||||
><el-card
|
||||
><div class="panel-title">采样记录数</div>
|
||||
<div class="kpi-value">{{ toNumber(qualityData.sampleCount) }}</div></el-card
|
||||
></el-col
|
||||
>
|
||||
<el-col :span="8"
|
||||
><el-card
|
||||
><div class="panel-title">覆盖率</div>
|
||||
<div class="kpi-value">{{ formatPercent(toNumber(qualityData.coverageRate)) }}</div></el-card
|
||||
></el-col
|
||||
>
|
||||
<el-col :span="8"
|
||||
><el-card
|
||||
><div class="panel-title">设备数量</div>
|
||||
<div class="kpi-value">{{ toNumber(qualityData.deviceCount) }}</div></el-card
|
||||
></el-col
|
||||
>
|
||||
</el-row>
|
||||
<el-card class="mt-4"><template #header>各指标有效率</template><Chart ref="qualityChart" class="chart-lg" /></el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Chart from '@/components/Charts/Chart.vue';
|
||||
import { getVibrationQualityData, type VibrationQualityPageVO } from '@/api/ems/report/vibrationBoard';
|
||||
import VibrationBoardFilter from '../components/VibrationBoardFilter.vue';
|
||||
import VibrationBoardDeviceTree from '../components/VibrationBoardDeviceTree.vue';
|
||||
import { useVibrationBoardQueryState } from '../components/useVibrationBoardQueryState';
|
||||
|
||||
defineOptions({ name: 'VibrationBoardQuality' });
|
||||
|
||||
const qualityChart = ref<any>();
|
||||
const {
|
||||
loading,
|
||||
treeLoading,
|
||||
monitorTreeOptions,
|
||||
treeProps,
|
||||
daterangeRecordTime,
|
||||
queryForm,
|
||||
loadTree,
|
||||
handleTreeNodeClick,
|
||||
setTimeRange,
|
||||
setSamplingInterval,
|
||||
setVibrationParam,
|
||||
buildQuery
|
||||
} = useVibrationBoardQueryState();
|
||||
const qualityData = ref<VibrationQualityPageVO>({ metricQualityItems: [] });
|
||||
const toNumber = (value: number | string | undefined) => Number(value ?? 0);
|
||||
const formatPercent = (value: number) => `${(Number(value || 0) * 100).toFixed(1)}%`;
|
||||
|
||||
const renderChart = () => {
|
||||
qualityChart.value?.setData({
|
||||
xAxis: { type: 'category', data: (qualityData.value.metricQualityItems || []).map((item) => item.label) },
|
||||
yAxis: { type: 'value', axisLabel: { formatter: '{value}%' } },
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
data: (qualityData.value.metricQualityItems || []).map((item) => Number((toNumber(item.validRate) * 100).toFixed(1))),
|
||||
itemStyle: { color: '#5b8ff9' }
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
const handleQuery = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await getVibrationQualityData(buildQuery());
|
||||
qualityData.value = data || { metricQualityItems: [] };
|
||||
await nextTick();
|
||||
renderChart();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadTree();
|
||||
await handleQuery();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.panel-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
.kpi-value {
|
||||
margin-top: 12px;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #1f2329;
|
||||
}
|
||||
.chart-lg {
|
||||
height: 420px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div v-loading="loading" class="app-container">
|
||||
<VibrationBoardFilter
|
||||
:selection-label="queryForm.selectionLabel"
|
||||
:time-range="daterangeRecordTime"
|
||||
:sampling-interval="queryForm.samplingInterval"
|
||||
:vibration-param="queryForm.vibrationParam"
|
||||
show-metric
|
||||
@update:time-range="setTimeRange"
|
||||
@update:sampling-interval="setSamplingInterval"
|
||||
@update:vibration-param="setVibrationParam"
|
||||
@query="handleQuery"
|
||||
/>
|
||||
|
||||
<el-row :gutter="16" class="mt-4">
|
||||
<el-col :span="6">
|
||||
<VibrationBoardDeviceTree :loading="treeLoading" :tree-data="monitorTreeOptions" :tree-props="treeProps" @node-click="handleTreeNodeClick" />
|
||||
</el-col>
|
||||
<el-col :span="18">
|
||||
<el-card
|
||||
><template #header>{{ trendData.multiDevice ? '设备对比趋势图' : '多指标趋势图' }}</template
|
||||
><Chart ref="trendChart" class="chart-xl"
|
||||
/></el-card>
|
||||
<el-card class="mt-4"><template #header>小时均值图</template><Chart ref="hourlyChart" class="chart-lg" /></el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Chart from '@/components/Charts/Chart.vue';
|
||||
import { getVibrationTrendData, type VibrationTrendPageVO } from '@/api/ems/report/vibrationBoard';
|
||||
import VibrationBoardFilter from '../components/VibrationBoardFilter.vue';
|
||||
import VibrationBoardDeviceTree from '../components/VibrationBoardDeviceTree.vue';
|
||||
import { useVibrationBoardQueryState } from '../components/useVibrationBoardQueryState';
|
||||
import { vibrationMetricOptions } from '../components/vibrationBoardShared';
|
||||
|
||||
defineOptions({ name: 'VibrationBoardTrend' });
|
||||
|
||||
const trendChart = ref<any>();
|
||||
const hourlyChart = ref<any>();
|
||||
const {
|
||||
loading,
|
||||
treeLoading,
|
||||
monitorTreeOptions,
|
||||
treeProps,
|
||||
daterangeRecordTime,
|
||||
queryForm,
|
||||
loadTree,
|
||||
handleTreeNodeClick,
|
||||
setTimeRange,
|
||||
setSamplingInterval,
|
||||
setVibrationParam,
|
||||
buildQuery
|
||||
} = useVibrationBoardQueryState();
|
||||
const trendData = ref<VibrationTrendPageVO>({ series: [], hourlyItems: [] });
|
||||
const metricColorMap = Object.fromEntries(vibrationMetricOptions.map((item) => [item.field, item.color]));
|
||||
const toNumber = (value: number | string | undefined) => Number(value ?? 0);
|
||||
|
||||
const renderCharts = () => {
|
||||
const series = trendData.value.series || [];
|
||||
trendChart.value?.setData({
|
||||
color: series.map((item) => metricColorMap[item.field || queryForm.value.vibrationParam] || '#5b8ff9'),
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { top: 8, data: series.map((item) => item.name) },
|
||||
xAxis: { type: 'time' },
|
||||
yAxis: { type: 'value', name: trendData.value.unit || '' },
|
||||
series: series.map((item) => ({
|
||||
name: item.name,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
connectNulls: true,
|
||||
showSymbol: false,
|
||||
data: (item.points || []).map((point) => [point.time, toNumber(point.value)])
|
||||
}))
|
||||
});
|
||||
hourlyChart.value?.setData({
|
||||
xAxis: { type: 'category', data: (trendData.value.hourlyItems || []).map((item) => item.hour) },
|
||||
yAxis: { type: 'value', name: trendData.value.unit || '' },
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
data: (trendData.value.hourlyItems || []).map((item) => toNumber(item.avgValue)),
|
||||
itemStyle: { color: metricColorMap[queryForm.value.vibrationParam] || '#5b8ff9' }
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
const handleQuery = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await getVibrationTrendData(buildQuery());
|
||||
trendData.value = data || { series: [], hourlyItems: [] };
|
||||
await nextTick();
|
||||
renderCharts();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadTree();
|
||||
await handleQuery();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-xl {
|
||||
height: 460px;
|
||||
}
|
||||
.chart-lg {
|
||||
height: 320px;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue