feat(振动报表): 新增振动报表功能模块

新增振动报表功能模块,包含以下组件和页面:
- 设备树组件(VibrationBoardDeviceTree.vue)
- 筛选组件(VibrationBoardFilter.vue)
- 共享工具函数(vibrationBoardShared.ts)
- 查询状态管理(useVibrationBoardQueryState.ts)
- 性能优化文档(performance-optimization.md)
- 六个报表页面(overview, trend, comparison, quality, distribution, anomaly)
- API接口文件(vibrationBoard.ts)

实现振动数据的多维度展示和分析功能,包括概览、趋势、对比、质量、分布和异常检测等报表页面
main
zch 3 months ago
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…
Cancel
Save